-
+
diff --git a/atom-excerpt.xml b/atom-excerpt.xml
index 5948e481bd..194b058b01 100644
--- a/atom-excerpt.xml
+++ b/atom-excerpt.xml
@@ -25,10 +25,10 @@
-
+
diff --git a/atom.xml b/atom.xml
index 9e96c366f0..72c301816a 100644
--- a/atom.xml
+++ b/atom.xml
@@ -25,10 +25,10 @@
-
+
@@ -118,7 +118,7 @@
<h2 id="The-Game"><a href="#The-Game" class="headerlink" title="The Game"></a>The Game</h2><p>The coach seemed to have had something similar in mind, because as soon as the ball started rolling, our boys attacked their opponents and had their first 100% chance after just 20 seconds (!). A few minutes later, the ball was actually in the goal, but it was probably disallowed by the referee for offside or something similar. I don’t know but it didn’t matter as long as it continued at this pace. Braunschweig were hopelessly out of their depth and simply tried to prevent the inevitable … until the 18th minute … 1:0 - A fine header by our defender <strong>Aleksandar Vukotic</strong> :)</p>
<p>However, the problem was that our team then let themselves go a little and only benefited from the opponent’s harmlessness until the break. As if the job was already done…</p>
- <div class="image-masonry" id="image-masonry-n3hseo">
+ <div class="image-masonry" id="image-masonry-k5yygd">
<div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_165816147.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_170229560.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_170946775.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_172204474.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_172706390.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_173932826.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_174727767.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_183243917.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_184445039.jpg" alt="" /></div>
</div>
@@ -202,7 +202,7 @@
<p>Not only because it felt like luxury camping without people but only water around, but also because it gave a pleasant feeling of freedom. Yes, we weren’t nearly alone on the lakes and getting a good spot in a marina for electricity and fresh water wasn’t always easy, but then just anchoring in any lake for the night and being woken up by birdsong and a gentle swell was magical.</p>
<p>Below are a few pictures from this trip, some of which will be used again as hero images in this blog:</p>
- <div class="photo-list" id="photo-list-d1g0gm">
+ <div class="photo-list" id="photo-list-16sdnb">
<figure>
<a href="/photos/23-08-Mecklenburg-Seen-0040" class="no-break">
@@ -513,7 +513,7 @@
<span id="more"></span>
- <div class="photo-list" id="photo-list-f9l40b">
+ <div class="photo-list" id="photo-list-2cn8v9">
<figure>
<a href="/photos/23-09-11-Speyer-0001" class="no-break">
@@ -610,7 +610,7 @@
<hr>
<h2 id="The-Game"><a href="#The-Game" class="headerlink" title="The Game"></a>The Game</h2><p>In the first half, the game was rather well balanced, even if you had the feeling that our team was the only ones playing. There was no sign of Kaiserlautern’s attacking drive. Up until the 39th minute, we had one chance to score, but the visitors hadn’t even had a shot on our goal. Nothing, nada, niente. But then the ball was in our net: <strong>0:1</strong>! A strange goal, because it didn’t look intentional. The ball just bounced at Ritter’s feet by chance and he simply took a shot. Goal. Damn…</p>
- <div class="image-masonry" id="image-masonry-em1qnj">
+ <div class="image-masonry" id="image-masonry-odjtmi">
<div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_120526904.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_122110857.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_122854839.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_122739398.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_122947255.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_135210240.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_141045409.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_142300311.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_142353462.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_142646533.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_142730319.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_142841643.jpg" alt="" /></div>
</div>
@@ -765,7 +765,7 @@
<p>Stritzel has saved fantastic balls this season, but not a penalty yet … until then, as he parried Kinsombi’s weak shot to the side. Yesss…!</p>
<p>Nothing much happened until half-time, but our guys were slowly gaining the upper hand, as the game was played almost exclusively in the opponent’s half at the beginning of the second half, with occasional counter-attacks by Rostock, but they were too erratic to have any effect. Our defence stood firm.</p>
- <div class="image-masonry" id="image-masonry-1o3k41">
+ <div class="image-masonry" id="image-masonry-empr3t">
<div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_122726572.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_122837500.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_121402556.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_123926894.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_140540817.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_141750690.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_142643939.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_142901228.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_142951071.jpg" alt="" /></div>
</div>
@@ -813,7 +813,7 @@
<hr>
<h2 id="The-Game"><a href="#The-Game" class="headerlink" title="The Game"></a>The Game</h2><p>The first 10 minutes of the game were unspectacular. They felt each other out and the Finns played diligently and should have been in the lead after 5 minutes, but got in the 9th minute after some guesswork from the referee with VAR support a penalty against them. </p>
- <div class="image-masonry" id="image-masonry-oz7qh4">
+ <div class="image-masonry" id="image-masonry-ydg4i0">
<div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_191040789.jpg" alt="" /></div><div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_191000090.jpg" alt="" /></div>
</div>
@@ -829,7 +829,7 @@
<p>In the break, the coach of the Finns seems to have been a little louder, because his guys played a little stronger and more determined, only to get another one in the 55th minute.</p>
<p>With 10 minutes to go, things got emotional in the stadium: the coach substituted Timothy Chandler, a Frankfurt veteran who has been with the club for almost 10 years and his first appearance this season. The tens of thousands of fans were completely out of their minds, screaming “Tiiiimmmyyy” as soon as he got to the ball. It was crazy. How must it feel to be showered with so much fan love! Things almost got out of hand when Chandler sprinted forward on the right flank in the 89th minute and made a wonderful pass into the middle and Dina Ebimbe just had to slot it in for 6:0.</p>
- <div class="image-masonry" id="image-masonry-whk9da">
+ <div class="image-masonry" id="image-masonry-2l3use">
<div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_183650025.jpg" alt="Attila, the Eintracht Eagle" /></div><div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_184409439.jpg" alt="Game Comic" /></div><div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_183658672.jpg" alt="Just before kickoff" /></div><div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_192703385.jpg" alt="2:0" /></div><div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_193136300.jpg" alt="Kickoff after 3:0" /></div><div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_205110961.jpg" alt="Final whistle" /></div><div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_205742073.jpg" alt="Lap of Honor" /></div>
</div>
@@ -967,7 +967,7 @@
<h2 id="The-Game"><a href="#The-Game" class="headerlink" title="The Game"></a>The Game</h2><p>HSV is actually in shape this year to make it back to the Bundesliga. They have already dropped a few points, but have been on the promotion places from the beginning. This year it should be something and accordingly dominant have the Hamburg the game against once again deep standing Wiesbadener also started. Very sure of the ball and winning almost every duel, they were unable to capitalize on this in the first half. They brought the ball very close to the goal, but not into it, and you could tell that the guests were getting a little more annoyed as time went on.</p>
<p>This was expressed shortly before the half-time break also in a few unsportsmanlike conduct. Dropping and claiming foul play is simply stupid and only provokes catcalls. The one or other time is overall bad referee but also fell for the trick or has made other nonsensical decisions, which caused the “right” fans around me to nastiest insults.</p>
- <div class="image-masonry" id="image-masonry-zs6k4c">
+ <div class="image-masonry" id="image-masonry-9to8wg">
<div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_103936756.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_104935878.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_105740138.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_105912858.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_110002467.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_110827714.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_111728897.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_123948926.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_125719562.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_125751495.jpg" alt="" /></div>
</div>
@@ -1002,7 +1002,7 @@
<p>Usually the best photos end up unnoticed as <a href="/post/Pool-Photo-Generator/" title="Pool Photo Generator">pool photos</a> here on my blog until I use one as a header image, but now I make a separate post out of each set before I publish the individual photos on other sites like <a href="https://500px.com/p/kikon">500px</a> or <a href="https://pixelfed.social/kristofz">Pixelfed</a>.</p>
- <div class="photo-list" id="photo-list-med46o">
+ <div class="photo-list" id="photo-list-1gg7tl">
<figure>
<a href="/photos/23-07-Mallorca-0300" class="no-break">
@@ -1323,7 +1323,7 @@
<p>In the meantime we were on two more concerts in Frankfurt, then in Marburg (Open-Air) and in Mainz on the “Night of the Ballads”. Most recently we saw them two weeks ago at the MPS (“Mittelalterlich Phantasie Spectaculum”, a medieval festival) in Speyer, where they played open-air with other bands from the “industry”. On the one hand, I think it’s great that the guys, even if they now fill large halls in Germany, have not forgotten their origins, and on the other hand, I now had the opportunity to get very close to the stage with my camera.</p>
<p>Here are the results:</p>
- <div class="image-masonry" id="image-masonry-j3et5x">
+ <div class="image-masonry" id="image-masonry-ferwmb">
<div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0092.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0094.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0103.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0104.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0105.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0106.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0108.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0111.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0115.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0123.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0126.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0128.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0131.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0136.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0150.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0151.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0152.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0153.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0156.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0157.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0159.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0162.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0169.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0171.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0172.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0173.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0177.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0179.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0182.jpg" alt="" /></div>
</div>
@@ -1360,7 +1360,7 @@
<p>It wasn’t, however, that Schalke dominated the game. We carelessly let them just do it and the fans had their fun when they shot the ball into the clouds again after a lot of roaring and stomping. Funny.</p>
<p>At the beginning of the second half, this continued seamlessly, even if you noticed that the coach in the cabin must have yelled at one or the other and these were now somewhat more active … and suddenly the ball was in our net … <strong>0:1</strong>! What? How? A goal in the 54. minute for Schalke out of (almost) nothing! I have to watch that again on TV … :|</p>
- <div class="image-masonry" id="image-masonry-694dqk">
+ <div class="image-masonry" id="image-masonry-25bgg5">
<div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_104957617.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_105327820.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_105347190.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_105548827.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_105749100.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_124411594.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_111931000.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_120730081.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_125820397.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_130050774.jpg" alt="" /></div>
</div>
@@ -1406,7 +1406,7 @@
<p><strong>Live Output:</strong></p>
- <div class="image-masonry" id="image-masonry-k0yq6b">
+ <div class="image-masonry" id="image-masonry-i7ci44">
<div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/D50_0053.jpg" alt="Thomas' Ruby Prince I" /></div><div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/_D50_3251.jpg" alt="No Name" /></div><div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/D50_0086.jpg" alt="Thomas' German Flag" /></div><div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/D50_1147.jpg" alt="Poppy Green" /></div><div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/D50_0075.jpg" alt="Thomas Wild Tulips" /></div><div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/D50_7474.jpg" alt="Garden Beauties XIV" /></div><div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/D50_4451.jpg" alt="Garden Beauties I" /></div><div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/D50_1577.jpg" alt="Floral Magic XIV" /></div>
</div>
diff --git a/categories/SQL/index.html b/categories/SQL/index.html
index f8683da2de..70779fa0c4 100644
--- a/categories/SQL/index.html
+++ b/categories/SQL/index.html
@@ -301,7 +301,7 @@
Tags
-
+
diff --git a/content.json b/content.json
index 52fbe82220..9d7c7e15a9 100644
--- a/content.json
+++ b/content.json
@@ -1 +1 @@
-{"meta":{"title":"kiko.io","subtitle":"Memorable (Tech) Stuff","description":"Blog about memorable (tech) stuff by Kristof Zerbe","author":"Kristof Zerbe","url":"https://kiko.io","root":"/"},"pages":[{"title":"","date":"2022-12-30","updated":"2022-12-30","path":"index.html","permalink":"https://kiko.io/index.html","excerpt":"","text":"TODO: Extract text from index.ejs"},{"title":"404","date":"2020-09-23","updated":"2020-09-23","path":"404.html","permalink":"https://kiko.io/404.html","excerpt":"","text":"I don’t know how you ended up here, but you have jumped over the edge of this blog. Maybe it’s the end of the internet and you can power off your machine now… /@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@ @@@@@@@@@ ,@@@@@ @@@@@@@@@@ @@@@@@@@@ @@@@@@@@@@ ,@@@@@@@@@@@@ @@@@@@@@@ @@@@@@@@@@@@ @@@@@@@@@@@@ @@@@@@@@@ (@@@@@@@@@@@@ #@@@@@@@@@@@ @@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@ @@@@@@@@@@, @@@@@@@@@. @@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@ @@@@@@@@@ @@@@@@@@@@ @@@@@@@@@ @@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@@@@@ /@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@ &@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@. *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@ … or you climb back and read one of my recent posts :)"},{"title":"Impressum","date":"2023-01-09","updated":"2023-01-09","path":"impressum/index.html","permalink":"https://kiko.io/impressum/index.html","excerpt":"","text":"The following information is required under German law. Verantwortlich für den Inhalt gemäß § 55 Abs.2 RStVKristof ZerbeKaiser-Friedrich-Str. 865193 Wiesbaden Mobil: +49 171 6998867 kristof.zerbe@gmail.com Rechtliche HinweiseHiermit wird darauf hingewiesen, dass Informationen auf dieser Site technische Ungenauigkeiten oder typografische Fehler enthalten können. Es gilt der Vorbehalt, dass die Informationen dieser Site jederzeit und ohne vorherige Ankündigung geändert oder aktualisiert werden können. Alle auf diesem Blog veröffentlichten Werke bzw. Werkteile wie z.B. Texte, Dateien, Kompositionen und Bilder sind urheberrechtlich geschützt. Jede weitere Veröffentlichung, Vervielfältigung, Verbreitung oder sonstige Nutzung – auch auszugsweise – bedarf der Zustimmung des Seitenbetreibers. Die Inhalte der Seiten wurden sorgfältig geprüft und nach bestem Wissen erstellt. Dennoch wird für die hier bereitgestellten Informationen kein Anspruch auf Vollständigkeit, Aktualität, Qualität und Richtigkeit erhoben. Für Schäden, die durch das Vertrauen auf die Inhalte dieser Website oder deren Gebrauch entstehen, wird keine Haftung übernommen. Dies gilt insbesondere für Inhalte verlinkter Webseiten, für deren Inhalte ausschließlich deren Betreiber verantwortlich sind. DatenschutzDie anwendbaren datenschutzrechtlichen Bestimmungen werden bei der Erhebung, bei der Nutzung und bei der Verarbeitung personenbezogener Daten beachtet. Sofern innerhalb des Internet-Angebotes die Möglichkeit der Eingabe von persönlichen Daten (E-Mail-Adresse oder Namen) besteht, erfolgt diese freiwillig. Die von Ihnen zur Verfügung gestellten personenbezogenen Daten werden nur intern zur Kommunikation mit Ihnen verwendet. HostingDiese Website wird auf GitHub Pages gehostet. Wenn eine GitHub Pages-Website besucht wird, wird die IP-Adresse des Besuchers zu Sicherheitszwecken protokolliert und gespeichert, unabhängig davon, ob sich der Besucher bei GitHub angemeldet hat oder nicht. Weitere Informationen zu den Sicherheitspraktiken von GitHub sind in den GitHub-Datenschutzbestimmungen zu finden. KommentareDie Kommentare auf dieser Website werden von der Open-Source-Bibliothek Utterances ermöglicht, das ebenfalls auf GitHub gehostet wird. Von dort wird der Kommentar-Client nachgeladen und auf der entsprechenden Artikelseite angezeigt. Um zu kommentieren ist eine Anmeldung mittels Ihres eigenen GitHub-Kontos notwendig. Die Kommentare werden im GitHub-Repository als sogenanntes ‘Issue’ an der Artikeldatei dieser Webseite mit einer Verknüpfung zu Ihren GitHub-Profilinformationen gespeichert. Weitere Informationen ergeben sich aus dem obigen Abschnitt ‘Hosting’. WebmentionsWebmentions sind ein Mechanismus, mit dem andere Websites benachrichtigt werden, wenn Sie auf Ihrer eigenen Website auf sie verweisen. Indem Sie Webmentions auf Ihrer Website unterstützen, signalisieren Sie ausdrücklich, dass Sie möchten, dass die verlinkten Websites Ihre öffentlichen Antworten auf deren Inhalte verarbeiten und veröffentlichen. Sie können jederzeit die Entfernung einer oder aller Webmentions, die von Ihrer Website stammen, beantragen. In der Regel werden empfangene Webmentions als Kommentare auf einer Webseite angezeigt. Dies bedeutet, dass eine Kopie Ihres Inhalts auf der Website angezeigt wird, auf die Sie in Ihrem eigenen Beitrag verlinken. Eine eingehende Webmention-Anfrage wird als Aufforderung zur Verarbeitung personenbezogener Daten behandelt und ist somit von vornherein eine Anfrage zur Veröffentlichung eines Kommentars von einer anderen Stelle im Web. Dafür wurde das Protokoll entwickelt und deshalb ist es auf Ihrer Website aktiv. Zu den veröffentlichten persönlichen Daten gehören Ihr Name, Ihr Profilbild von Ihrer Website oder einem gleichartigen Dienst, die URL Ihrer Website und persönliche Informationen, die Sie möglicherweise in Ihrem Beitrag angegeben haben. Die Veröffentlichung eingehender Webmentions basiert auf dem berechtigten Interesse, die Interaktion mit den Lesern dieser Website zu ermöglichen (Art. 6 (1) DSGVO), und folgt der Designabsicht des Webmention-Protokolls. Analyse-Tool PirschAuf dieser Website kommt das Open-Source-Webanalysedienst Pirsch.io der Emvi Software GmbH, Nickelstraße 1b, 33378 Rheda-Wiedenbrück mit Sitz in Deutschland zum Einsatz, um die Nutzung der Website analysieren und regelmäßig verbessern zu können. Die Technologie von Pirsch ist eine Cookie-freie und datenschutzfreundliche Web-Analytik, um unsere Traffic genau messen zu können. Pirsch wird in Deutschland entwickelt und gehostet und arbeitet somit nach den strengen europäischen Datenschutzgesetzen der DSGVO. Die Analysedaten, welche erhoben werden, werden zu keiner Zeit an Dritte weitergegeben. Es werden mit der Technologie von Pirsch ausschließlich anonymisierte Daten (z.B. Datum und Uhrzeit des Seitenaufrufs, Verweildauer oder die Seite, von der sie unsere Website angesteuert haben) gespeichert und verwendet. Sie erlauben keine Identifikation der Besucher dieser Website. Rechtsgrundlage für die Nutzung von Pirsch ist Art. 6 Abs. 1 f) DSGVO. Sie haben die Möglichkeit, der Analyse zu widersprechen. Informationen zum Datenschutz von Pirsch erhalten sie unter: https://pirsch.io/privacy."},{"title":"","date":"2023-12-12","updated":"2023-12-12","path":"downloads/code/test.js","permalink":"https://kiko.io/downloads/code/test.js","excerpt":"","text":"alert('This is a test...'); console.log('This is a test...');"}],"posts":[{"title":"SVWW vs. Braunschweig @ 2023-12-08","subtitle":"Third defeat in a row, this time at home","series":"SV Wehen Wiesbaden","date":"2023-12-10","updated":"2023-12-10","path":"post/SVWW-vs-Braunschweig-2023-12-08/","permalink":"https://kiko.io/post/SVWW-vs-Braunschweig-2023-12-08/","excerpt":"1:3 Our last home game of the year. The last two away games against Greuther Fürth and Holstein Kiel were unfortunately lost. One 2:0 and the other 3:2, so you could say that our good run had come to an end. But well … anyone who thought it would go on like this should have seen a doctor. After all, we’re the promoted team and we have to win against far stronger teams with far more Bundesliga experience. We can only ever catch them at a weak moment and hope that in the end it will be enough to stay in the 2. Bundesliga. Eintracht Braunschweig was one of the teams we thought had a chance, as they were in a relegation spot with just 8 points before the matchday (we have 21) and hadn’t won an away game for months.","keywords":"home game year games greuther fürth holstein kiel lost good run end … thought doctor promoted team win stronger teams bundesliga experience catch weak moment hope stay eintracht braunschweig chance relegation spot points matchday hadnt won months","text":"1:3 Our last home game of the year. The last two away games against Greuther Fürth and Holstein Kiel were unfortunately lost. One 2:0 and the other 3:2, so you could say that our good run had come to an end. But well … anyone who thought it would go on like this should have seen a doctor. After all, we’re the promoted team and we have to win against far stronger teams with far more Bundesliga experience. We can only ever catch them at a weak moment and hope that in the end it will be enough to stay in the 2. Bundesliga. Eintracht Braunschweig was one of the teams we thought had a chance, as they were in a relegation spot with just 8 points before the matchday (we have 21) and hadn’t won an away game for months. It’s already very cold in Wiesbaden at the beginning of December, especially as the weather was bringing in a lot of cold air from the north, so I left the scooter and took a cab to the Brita Arena. Freezing sucks and as I no longer have the heat of the youth, I bought long underwear for winter stadium visits. (Uhh, the old man wears long underpants). I was glad to have them though, because on the one hand I was there way too early and on the other hand you end up sitting for most of the 120 minutes that a game like this lasts, including waiting for kick-off and the interval. The hot cider and warm beef sausages only helped to combat the cold to a limited extent. Bärbel did better, as she arrived shortly before kick-off, like most of the 7,200 spectators. We chatted a bit about trivialities and hoped that our boys wouldn’t let the opportunity pass them by today. The GameThe coach seemed to have had something similar in mind, because as soon as the ball started rolling, our boys attacked their opponents and had their first 100% chance after just 20 seconds (!). A few minutes later, the ball was actually in the goal, but it was probably disallowed by the referee for offside or something similar. I don’t know but it didn’t matter as long as it continued at this pace. Braunschweig were hopelessly out of their depth and simply tried to prevent the inevitable … until the 18th minute … 1:0 - A fine header by our defender Aleksandar Vukotic :) However, the problem was that our team then let themselves go a little and only benefited from the opponent’s harmlessness until the break. As if the job was already done… let macy_n3hseo = new Macy({ container: '#image-masonry-n3hseo', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); Bärbel and I hoped that the coach in the dressing room would be able to shake the boys up again and they would play the second half with the same energy of the first 20 minutes. But what came was a bitter disappointment. Braunschweig’s coach seemed to have achieved exactly what we had hoped for. Our opponents came onto the pitch and started playing aggressive football. In the 48th minute, the score was 1:1 after a counterattack and only 8 minutes later, Braunschweig had turned the game around: 1:2. The lack of resistance and insecurity on our side was problematic. Hardly a ball got to where it needed to go, hardly a duel was won and even 5 substitutions unfortunately did nothing to change this. The boys were somehow beside themselves and so Braunschweig put the game to bed in the 76th minute: 1:3 :| ⇾ Match report on kicker.de ConclusionThe stadium has rarely emptied as quickly as it did this time, but that wasn’t just due to the cold, but also the disappointment. Unfortunately, it wasn’t a Christmas present from the players to their fans in the last home game of 2023. The team have to play one last away game next week at St. Pauli before the break, but nobody expects a win against the strongest team in the 2nd division at the moment. No matter … we are in mid-table, which is more than we had hoped for at the start of the season. In 7 weeks time, we’ll be back in the Brita-Arena against Herta BSC and we’ll see better games again and stay in the league!","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"},{"name":"2. Bundesliga","slug":"2-Bundesliga","permalink":"https://kiko.io/tags/2-Bundesliga/"}]},{"title":"Hexo, WebFinger and better discoverability","subtitle":"Providing a static Webfinger file via your own domain","series":"A New Blog","date":"2023-12-02","updated":"2023-12-02","path":"post/Hexo-Webfinger-and-better-discoverability/","permalink":"https://kiko.io/post/Hexo-Webfinger-and-better-discoverability/","excerpt":"Recently I read the blog post “Mastodon on your own domain without hosting a server” by Maarten Balliauw, which dealt with how to become more visible in the Fediverse, more precisely in Mastodon, with your own domain, because in contrast to the Indieweb approach, the Fediverse relies on Actors (@USER@INSTANCE) of the respective instance/platform and can only include your own domain, if it becomes a Fediverse endpoint itself. In my case, the latter is not possible because this blog is a static site, generated via Hexo and hosted on GitHub. It simply lacks a modifiable active server component. However, Maarten has found a trick to at least make it findable in Mastadon via his own domain. First, he explains how Fediverse platforms work in general: – Mastodon (and others) use ActivityPub as their protocol to communicate between “actors”.– Actors are discovered using WebFinger, a way to attach information to an email address, or other online resource.– WebFinger lives on /.well-known/webfinger on a server. His idea was to simply copy the WebFinger file to his server and make it available in the same way, to allow the Fediverse server to find the correct actor, so search for @me@mydomain.xxx and find @me@my-fediverse-instance.xxx. Copy a file and deliver it via Hexo over .wellknown/webfinger? What can be so difficult about that…","keywords":"recently read blog post mastodon domain hosting server maarten balliauw dealt visible fediverse precisely contrast indieweb approach relies actors @user@instance respective instance/platform include endpoint case static site generated hexo hosted github simply lacks modifiable active component found trick make findable mastadon explains platforms work general – activitypub protocol communicate actors– discovered webfinger attach information email address online resource– lives /well-known/webfinger idea copy file find correct actor search @me@mydomainxxx @me@my-fediverse-instancexxx deliver wellknown/webfinger difficult that…","text":"Recently I read the blog post “Mastodon on your own domain without hosting a server” by Maarten Balliauw, which dealt with how to become more visible in the Fediverse, more precisely in Mastodon, with your own domain, because in contrast to the Indieweb approach, the Fediverse relies on Actors (@USER@INSTANCE) of the respective instance/platform and can only include your own domain, if it becomes a Fediverse endpoint itself. In my case, the latter is not possible because this blog is a static site, generated via Hexo and hosted on GitHub. It simply lacks a modifiable active server component. However, Maarten has found a trick to at least make it findable in Mastadon via his own domain. First, he explains how Fediverse platforms work in general: – Mastodon (and others) use ActivityPub as their protocol to communicate between “actors”.– Actors are discovered using WebFinger, a way to attach information to an email address, or other online resource.– WebFinger lives on /.well-known/webfinger on a server. His idea was to simply copy the WebFinger file to his server and make it available in the same way, to allow the Fediverse server to find the correct actor, so search for @me@mydomain.xxx and find @me@my-fediverse-instance.xxx. Copy a file and deliver it via Hexo over .wellknown/webfinger? What can be so difficult about that… Download the WebFinger fileAs Maarten describes, the WebFinger file (JSON) can always be found on a URL according to the following pattern:https://<your mastodon server>/.well-known/webfinger?resource=acct:<your account>@<your mastodon server>. I have my Mastodon account ‘kiko’ at indieweb.social and the path to the download was accordingly: https://indieweb.social/.well-known/webfinger?resource=acct:kiko@indieweb.social. Solution 1 - Copy and provide as static fileFirst of all, Hexo knows nothing about static files. You have to teach it, for example, using the hexo-generator-copy plugin, which I did a long time ago because I deliver a lot of such files, like images, photos, manifests and so on. Including the WebFinger file in the .wellknown folder in the generation was therefore an obvious choice … but did not work, as the plugin ignores all files and folders that begin with a dot or underscore. This doesn’t matter on Windows, but it probably does on other platforms, so I wanted to leave the plugin’s code untouched and wrote another generator that then generates the file separately into the output: generator-wellknown-webfinger.js1234567891011121314151617181920212223242526const log = require('hexo-log')({ debug: false, silent: false });const path = require('path');const fs = require('hexo-fs');hexo.extend.generator.register("wellknown-webfinger", async function() { log.info("Processing .well-known/webfinger ..."); const _rootPath = hexo.base_dir; const _path = ".well-known/webfinger"; let content = ""; let filePath = path.join(_rootPath, this.config.static_dir, _path); if (fs.existsSync(filePath)) { json = JSON.parse(fs.readFileSync(filePath)); content = JSON.stringify(json); // flatten JSON } let result = { data: content, path: _path }; return result;}); The whole thing is not magic, because it simply reads the local file from /static/.wellknown/webfinger and returns the output path and the flattened content to be rendered as the result. But somehow it didn’t feel right to first download a file manually and then bypass a generator with another generator in order to deliver the file. Solution 2 - Download and deliver during generationA short time later, I deleted the local file again and rewrote the generator, because the first solution didn’t make sense to me. Generating my blog now takes less than a minute anyway, so it doesn’t matter whether I fetch the WebFinger file directly from the server and write it straight to the output via hexo.route.set: generator-wellknown-webfinger.js123456789101112131415161718const log = require('hexo-log')({ debug: false, silent: false });const axios = require("axios");hexo.extend.generator.register("wellknown-webfinger", async function() { log.info("Processing .well-known/webfinger ..."); const url = `https://${this.config.mastodon.server}/.well-known/webfinger?resource=acct:${this.config.mastodon.user}@${this.config.mastodon.server}`; const _path = ".well-known/webfinger"; axios.get(url).then(response => { let json = response.data; hexo.route.set(_path, json); });}); The ResultAfter deploying to GitHub I was able to search on a Fediverse platform for @kristof@kiko.io and the result is the corresponding account of my Mastodon instance. :) However, this currently only works with one instance and the user specification is arbitrary (as Maarten also notes). For example, if I search on Pixelfed, my Mastodon account is also displayed, but not my Pixelfed account, which is also available there, because I have not integrated Pixelfeds WebFinger file. As mentioned above, the Fediverse works a little differently than the Indieweb. My wish would be to find something that identifies me with my domain everywhere. More Info IndieWebCamp: WebFingerBrandon Rozek: Mastodon/Webfinger Alias using HTTP RedirectsGitHub Issue from r3pek at webfinger.net: Is webfinger able to distinguish between 2 services on the same URI?","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Fediverse","slug":"Fediverse","permalink":"https://kiko.io/tags/Fediverse/"},{"name":"IndieWeb","slug":"IndieWeb","permalink":"https://kiko.io/tags/IndieWeb/"},{"name":"Identity","slug":"Identity","permalink":"https://kiko.io/tags/Identity/"}]},{"title":"Mecklenburg Lakes","subtitle":"Impressions of a Houseboat Trip","series":"New Photos","date":"2023-11-26","updated":"2023-11-26","path":"post/Mecklenburg-Lakes/","permalink":"https://kiko.io/post/Mecklenburg-Lakes/","excerpt":"August in Germany is usually a safe bet when it comes to the weather, so my wife and I decided to relax for a few days on the Mecklenburg Lake Plateau … on a chartered boat. In general, you need a driving license for everything that moves in this country, but there are a few exceptions, such as in Brandenburg/Mecklenburg where even inexperienced tourists are allowed to steer a 12-metre houseboat/yacht across the many lakes after a short briefing. The weather was, let’s say, suboptimal this August, because a storm passed over us in the first three days and there was no chance of happy sailing around on the water. On the one hand, the swell and wind forces were extremely worrying for newbies and on the other, it’s simply no fun being on a boat in cold, heavy rain. We were stuck in Rheinsberg. On the one hand, the swell and wind forces were extremely worrying for newbies and on the other, it’s simply no fun being on a boat in cold, heavy rain.","keywords":"august germany safe bet weather wife decided relax days mecklenburg lake plateau … chartered boat general driving license moves country exceptions brandenburg/mecklenburg inexperienced tourists allowed steer 12-metre houseboat/yacht lakes short briefing lets suboptimal storm passed chance happy sailing water hand swell wind forces extremely worrying newbies simply fun cold heavy rain stuck rheinsberg","text":"August in Germany is usually a safe bet when it comes to the weather, so my wife and I decided to relax for a few days on the Mecklenburg Lake Plateau … on a chartered boat. In general, you need a driving license for everything that moves in this country, but there are a few exceptions, such as in Brandenburg/Mecklenburg where even inexperienced tourists are allowed to steer a 12-metre houseboat/yacht across the many lakes after a short briefing. The weather was, let’s say, suboptimal this August, because a storm passed over us in the first three days and there was no chance of happy sailing around on the water. On the one hand, the swell and wind forces were extremely worrying for newbies and on the other, it’s simply no fun being on a boat in cold, heavy rain. We were stuck in Rheinsberg. On the one hand, the swell and wind forces were extremely worrying for newbies and on the other, it’s simply no fun being on a boat in cold, heavy rain. Not only because it felt like luxury camping without people but only water around, but also because it gave a pleasant feeling of freedom. Yes, we weren’t nearly alone on the lakes and getting a good spot in a marina for electricity and fresh water wasn’t always easy, but then just anchoring in any lake for the night and being woken up by birdsong and a gentle swell was magical. Below are a few pictures from this trip, some of which will be used again as hero images in this blog: Grinning Horse Sea Magic Rainy Tourism Rainy Planks Chrome Bollard Rheinsberg Palace Pink Lotus Stony Vegetation Golden Drama Buddy Support Stormy Lake Green Canal Boiler Mirroring Into the Storm Sunset Jetty Golden Surfing Blue Lake Green Ducks Sparkling Duck Mirrored Boathouses","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"Discoveries #27 - JavaScript Tools","subtitle":null,"series":"Discoveries","date":"2023-11-20","updated":"2023-11-20","path":"post/Discoveries-27-JavaScript-Tools/","permalink":"https://kiko.io/post/Discoveries-27-JavaScript-Tools/","excerpt":"This month's Discoveries are about small, large, new and somewhat older JavaScript tools that can make life and coding easier for developers. Why reinvent the wheel when someone has already done it. Happy Coding :) Spacing.js - MeasuringUkiyo.js - Parallax EffectIntersectionObserver Debuggermande - Fetch WrapperVest - Declarative Validations FrameworkGranim.js - Gradient AnimationRoughNotationbarba.js - Page Transitionsdead-or-alive - URL Checkertimeago - Format Date","keywords":"month's discoveries small large older javascript tools make life coding easier developers reinvent wheel happy spacingjs - measuringukiyojs parallax effectintersectionobserver debuggermande fetch wrappervest declarative validations frameworkgranimjs gradient animationroughnotationbarbajs page transitionsdead-or-alive url checkertimeago format date","text":"This month's Discoveries are about small, large, new and somewhat older JavaScript tools that can make life and coding easier for developers. Why reinvent the wheel when someone has already done it. Happy Coding :) Spacing.js - MeasuringUkiyo.js - Parallax EffectIntersectionObserver Debuggermande - Fetch WrapperVest - Declarative Validations FrameworkGranim.js - Gradient AnimationRoughNotationbarba.js - Page Transitionsdead-or-alive - URL Checkertimeago - Format Date Spacing.js - Measuring by Steven Lei https://spacingjs.com/ Have you ever wanted to measure your web layout during development? Steven has a solution for you. Either as JavaScript integrated in HTML or as a Chrome plugin. Ukiyo.js - Parallax Effect by Yiteng Jun https://github.com/yitengjun/ukiyo-js This ES6 library is an easy to use tool for creating efficient background parallax effects for <img>, <picture>, <video> elements and background images. It can be called manually via JavaScript or automatically via a special class or data attribute. IntersectionObserver Debugger by Rodrigo Pombo https://github.com/pomber/intersection-observer-debugger This tiny script is a debugging tool, included during development, which shows you the root, target, and intersection every time an IntersectionObserver is triggered. mande - Fetch Wrapper by Eduardo San Martin Morote https://github.com/posva/mande With mande Eduardo has written a great wrapper for JavaScripts fetch(), which not only relieves you of a lot of typing work, but also comes with a few useful extensions, e.g. for nuxt. This turns an API call into a one-liner. Vest - Declarative Validations Framework by Evyatar Alush https://github.com/ealush/vest Vest is a declarative validation framework for validating form input that is as easy to use as the Mocha or Jest unit test libraries. Granim.js - Gradient Animation by Benjamin Blonde https://sarcadass.github.io/granim.js/examples.html Benjamin has created a library to animate gradients as complex as you need them. It supports pausing when it's not in view and different blending modes for images. RoughNotation by Preet Shihn https://roughnotation.com/ To emphasize something important on a sheet of paper, you sometimes use a highlighter. This library brings that to the web in an animated way. With a circle around it, underlined … or both, you're sure to attract attention. barba.js - Page Transitions by Multiple Contributors https://barba.js.org/features/user-friendly/ Barba is a library that allows you to create fluid and smooth transitions between pages on your website, while we wait for the View Transitions API. dead-or-alive - URL Checker by Titus Wormer https://github.com/wooorm/dead-or-alive This URL checker ensures that links do not lead to nothing, whether on websites, in node projects or in service workers. It even checks anchor links for the presence of the element. timeago - Format Date by Whoever Titanium is... https://github.com/Titanium2099/timeago 'Your message was sent 5 minutes ago'. Notes like these are easy to bring into a web application with this script. It currently only supports English, but is easily customizable.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Speyer Automotive","subtitle":null,"series":"New Photos","date":"2023-11-15","updated":"2023-11-15","path":"post/Speyer-Automotive/","permalink":"https://kiko.io/post/Speyer-Automotive/","excerpt":"In September, my wife and I took a trip to the Museum of Technology in nearby Speyer. I don’t really know why I’ve never been there before, not even as a child, because the collection of cars and aeroplanes is quite unique in Germany. A dream for every boy, even if he’s not as young as he might think :) Built on the old site of an aircraft manufacturer, the 100-year-old hangars provide the perfect space for exhibiting large vehicles with wheels and wings. And exactly this can be found in Speyer: A large part for cars of all kinds and the outside area for aeroplanes and even ships. Even a spaceship, the Russian Space Shuttle alternative Buran can be seen there. Here are a few pictures of the car exhibition that I brought back from there and which will be used as post header images hero on kiko.io in the future …","keywords":"september wife trip museum technology nearby speyer dont ive child collection cars aeroplanes unique germany dream boy hes young built site aircraft manufacturer 100-year-old hangars provide perfect space exhibiting large vehicles wheels wings found part kinds area ships spaceship russian shuttle alternative buran pictures car exhibition brought back post header images hero kikoio future …","text":"In September, my wife and I took a trip to the Museum of Technology in nearby Speyer. I don’t really know why I’ve never been there before, not even as a child, because the collection of cars and aeroplanes is quite unique in Germany. A dream for every boy, even if he’s not as young as he might think :) Built on the old site of an aircraft manufacturer, the 100-year-old hangars provide the perfect space for exhibiting large vehicles with wheels and wings. And exactly this can be found in Speyer: A large part for cars of all kinds and the outside area for aeroplanes and even ships. Even a spaceship, the Russian Space Shuttle alternative Buran can be seen there. Here are a few pictures of the car exhibition that I brought back from there and which will be used as post header images hero on kiko.io in the future … Red Bull 12 Packard Monster Golden Cooler Vespa Treasure Yellow Curves Red Motor Cover Maybach Zeppelin Mercedes Gold Magirus Red","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"SVWW vs. Kaiserslautern @ 2023-11-12","subtitle":"The winning streak continues in curious fashion","series":"SV Wehen Wiesbaden","date":"2023-11-15","updated":"2023-11-15","path":"post/SVWW-vs-Kaiserslautern-2023-11-12/","permalink":"https://kiko.io/post/SVWW-vs-Kaiserslautern-2023-11-12/","excerpt":"2:1 Travelling by scooter in Germany in the middle of November is no fun. Just like on Sunday. Light rain at just under 9 degrees Celsius and quite a bit of wind. Brrr… but what can I do, there are no car parks at the arena. Not at all today, because it’s against Kaiserslautern, which is only about 100 kilometres away from Wiesbaden and therefore a lot of visitors are expected. It’s a kind of derby. 1. FC Kaiserslautern is one of the great traditional clubs in Germany, which has celebrated a total of 4 German Championships in its history, but has really lost momentum since its relegation to the 2. Bundesliga in 2006. Between 2018 and 2022, the team even only played in the 3rd division pf German football, but after their last year’s promotion, they are hoping to be promoted to the top division again soon. Recent results in the cup have shown that they have a strong team and it was to be feared that they would overrun Wehen Wiesbaden in their run, although their last league game was lost against Fürth. It was also rather uncomfortably cold in the stadium and I wondered once again how the young lads do it on the pitch in their shorts, especially the goalkeeper. But the fans created the right atmosphere. As expected, the entire guest stand was full. 12,100 spectators in total; a new season record. The SVWW fan block was also packed this time and the ultras had come up with a nice choreo with complete stand banners and red smoke (see photos).","keywords":"travelling scooter germany middle november fun sunday light rain degrees celsius bit wind brrr… car parks arena today kaiserslautern kilometres wiesbaden lot visitors expected kind derby fc great traditional clubs celebrated total german championships history lost momentum relegation bundesliga team played 3rd division pf football years promotion hoping promoted top recent results cup shown strong feared overrun wehen run league game fürth uncomfortably cold stadium wondered young lads pitch shorts goalkeeper fans created atmosphere entire guest stand full spectators season record svww fan block packed time ultras nice choreo complete banners red smoke photos","text":"2:1 Travelling by scooter in Germany in the middle of November is no fun. Just like on Sunday. Light rain at just under 9 degrees Celsius and quite a bit of wind. Brrr… but what can I do, there are no car parks at the arena. Not at all today, because it’s against Kaiserslautern, which is only about 100 kilometres away from Wiesbaden and therefore a lot of visitors are expected. It’s a kind of derby. 1. FC Kaiserslautern is one of the great traditional clubs in Germany, which has celebrated a total of 4 German Championships in its history, but has really lost momentum since its relegation to the 2. Bundesliga in 2006. Between 2018 and 2022, the team even only played in the 3rd division pf German football, but after their last year’s promotion, they are hoping to be promoted to the top division again soon. Recent results in the cup have shown that they have a strong team and it was to be feared that they would overrun Wehen Wiesbaden in their run, although their last league game was lost against Fürth. It was also rather uncomfortably cold in the stadium and I wondered once again how the young lads do it on the pitch in their shorts, especially the goalkeeper. But the fans created the right atmosphere. As expected, the entire guest stand was full. 12,100 spectators in total; a new season record. The SVWW fan block was also packed this time and the ultras had come up with a nice choreo with complete stand banners and red smoke (see photos). My nice seat neighbour, the older lady, was there again and we made friends this time. Her name is Bärbel and this time she was a bit more excited than normally. Well, the aim was to beat Kaiserslautern. Kaiserslautern! Every now and then she also started to grumble: about an opposing player or when things didn’t go fast enough forwards or when the ball was lost unnecessarily. Not nearly as blatant as the “gentlemen” behind us, who were swearing in one piece and indulging in football wisdom, but you could tell that the game was more important to her than others. The GameIn the first half, the game was rather well balanced, even if you had the feeling that our team was the only ones playing. There was no sign of Kaiserlautern’s attacking drive. Up until the 39th minute, we had one chance to score, but the visitors hadn’t even had a shot on our goal. Nothing, nada, niente. But then the ball was in our net: 0:1! A strange goal, because it didn’t look intentional. The ball just bounced at Ritter’s feet by chance and he simply took a shot. Goal. Damn… let macy_em1qnj = new Macy({ container: '#image-masonry-em1qnj', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); After the break, Kaiserslautern seemed to want to build on the goal. They were now playing football. And ours were probably angry at not having been rewarded for their efforts in the first half. The game became more lively, but we still had the better scenes … and after just 4 minutes, the visitors gave our Tijmen Goppel far too much time to aim and shoot just outside the penalty area and the ball was deflected into the goal by an opponent’s leg. 1:1! In the 65th minute, things got curious again: Goppel was fouled and awarded a free kick about 20 metres from goal. Robin Heußer took it and his shot somehow hit the chest of Ivan Prtajin in the penalty area, from where the ball went into the goal. 2:1 … Game turned round! The last half hour was really nerve-wracking. We continued to play better, but the pressure increased and the game became noticeably faster. Shortly before the end, substitute John Iredale ran alone with the ball across half the pitch and everyone held their breath, but just before the goal he tried to round the last defender and he was just able to scoop the ball away. G… damn it! ⇾ Match report on kicker.de ConclusionThis was the fourth (!) game in a row that we have won and our position in the table looks correspondingly favourable again. Only three points behind the promotion places. Woohoo … but let’s not fool ourselves: There will be another losing streak in the future (as always in football) and 21 points are still not enough to keep us in the league, and that’s all that matters. Two away games are scheduled in the next three weeks, against Greuther Fürth and Holstein Kiel. On 8 December, we return to the Brita Arena against Eintracht Braunschweig, currently second last in the table. They should be beatable…","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"},{"name":"2. Bundesliga","slug":"2-Bundesliga","permalink":"https://kiko.io/tags/2-Bundesliga/"}]},{"title":"Add Link to Trello on Android via Share Menu","subtitle":null,"date":"2023-11-13","updated":"2023-11-13","path":"post/Add-Link-to-Trello-on-Android-via-Share-Menu/","permalink":"https://kiko.io/post/Add-Link-to-Trello-on-Android-via-Share-Menu/","excerpt":"I have been collecting interesting links in various Trello boards for many years and also process some of them automatically, such as my Tiny Tools. However, the official Trello Android App has the problem eversince that, when you want to create a URL as a Trello card in the browser, the URL is entered in the Description and not as an Attachment, where it actually belongs. I have solved this for myself for years using a bookmarklet in the Chrome browser (see Add website to Trello card the better way) and get along quite well with it. However, there is one catch, that has annoyed me ever since:I find an interesting link in the Mastodon WebApp, for example, and tap on it. What opens, however, is the WebView integrated in Android and not my standard Chrome browser, in which the bookmarklet would be available. So, for links that I want to store, I always have to open the WebView menu and select “Open in Chrome Browser”. I cannot use the general SHARE menu. At least not so far … :)","keywords":"collecting interesting links trello boards years process automatically tiny tools official android app problem eversince create url card browser entered description attachment belongs solved bookmarklet chrome add website catch annoyed sincei find link mastodon webapp tap opens webview integrated standard store open menu select general share …","text":"I have been collecting interesting links in various Trello boards for many years and also process some of them automatically, such as my Tiny Tools. However, the official Trello Android App has the problem eversince that, when you want to create a URL as a Trello card in the browser, the URL is entered in the Description and not as an Attachment, where it actually belongs. I have solved this for myself for years using a bookmarklet in the Chrome browser (see Add website to Trello card the better way) and get along quite well with it. However, there is one catch, that has annoyed me ever since:I find an interesting link in the Mastodon WebApp, for example, and tap on it. What opens, however, is the WebView integrated in Android and not my standard Chrome browser, in which the bookmarklet would be available. So, for links that I want to store, I always have to open the WebView menu and select “Open in Chrome Browser”. I cannot use the general SHARE menu. At least not so far … :) The bookmarklet does nothing more as sending the link with some additional information like name (title) to the URL https://trello.com/add-card, where the user can select the desired board and list: Trello-AddCard-Bookmarklet.js1234567891011121314151617javascript: (function (win, name, desc) { win.open( "https://trello.com/add-card" + "?source=" + win.location.host + "&mode=popup" + "&url=" + encodeURIComponent(win.location.href) + (name ? "&name=" + encodeURIComponent(name) : "") + (desc ? "&desc=" + encodeURIComponent(desc) : ""), "add-trello-card", "width=500,height=600,left=" + (win.screenX + (win.outerWidth - 500) / 2) + ",top=" + (win.screenY + (win.outerHeight - 740) / 2) );})(window, document.title, getSelection ? getSelection().toString() : ""); If you want to use this bookmarklet, you have to remove all line breaks from the code, decode the url and save it as a bookmark … or you visit https://trello.com/add-card and drag the link “Send to Trello” to your bookmark list. It comes out the same. As I said above, the Trello Bookmarklet e.g. Add Card page works differently to the Android app from the same company in terms of the URL. Are different departments not talking to each other? However … for me, the bookmarklet is history, because there is a much better approach that uses the Add Card page also, but can be called up from the standard Android Share Menu. Power Tool: HTTP ShortcutsThe trigger for my new solution was my search for better interaction possibilities from my blog to the IndieWeb. One hit was the article Android IndieWeb interactions with the HTTP Shortcuts app by Ryan Barrett, which uses the app HTTP Shortcuts by Roland Meyer to serve as a Share Target for LIKE, FOLLOW and REPLY actions. Share Target? A while ago I had the idea of using the Web Share Target API to solve my problem above, but haven’t got round to setting up such a PWA yet. But I no longer have to do that, because the HTTP Shortcuts app not only offers me the option of serving as a share target, but also significantly more functions for creating Android shortcuts, which are extremely useful: Browser Shortcut - Open the URL in the browser Scripting Shortcut - Write JavaScript for advanced workflows Multi-Shortcut - Trigger multiple shortcuts at once All these shortcuts can be used to send any HTTP requests and process the responses in a variety of ways. The feature list is impressive and well documented. Scripting Shortcut for calling Trello’s Add Card PageFor my case, I need a scripting shortcut to process the information shared by the Android sharing dialogue to call the Add Card page. This is done via static variables that are created in HTTP Shortcuts and can then be used later in the script: Creating the shortcut is quite simple, as you can see from the screenshots. Select the type, assign a name, perhaps a suitable icon and (importantly) tick the option Show as app shortcut on launcher and promote as Direct Share target, which ensures that the shortcut appears in the Android Share menu. The script is no less straightforward, because it simply assembles the URL to the Trello Add Card page with the static variables and opens it using the app’s built-in JavaScript function openUrl: 123456789101112131415const url = getVariable("SharedUrl");const title = getVariable("SharedTitle");const getHostnameFromRegex = (url) => { const matches = url.match(/^https?\\:\\/\\/([^\\/?#]+)(?:[\\/?#]|$)/i); return matches && matches[1];}const hostname = getHostnameFromRegex(url);const trelloAddUrl = "https://trello.com/add-card?source=" + hostname + "&mode=popup&url=" + encodeURIComponent(url) + "&name=" + encodeURIComponent(title);openUrl(trelloAddUrl); ConclusionThe result is convincing and frees me from the bookmarklet on my smartphone, but I will continue to use it on my desktop browser. Download Shortcut Files for Import in HTTP Shortcut AddToTrello-Shortcut.zip The HTTP Shortcuts app, however, has really got me hooked. I see a lot of potential for automating things on my smartphone and will keep you updated as I develop more solutions with it. Thank you Roland for this gem …","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Trello","slug":"Trello","permalink":"https://kiko.io/tags/Trello/"},{"name":"Android","slug":"Android","permalink":"https://kiko.io/tags/Android/"}]},{"title":"Experimenting with the font LEXEND","subtitle":"Easier reading for users and more beautiful typography","date":"2023-11-12","updated":"2023-11-12","path":"post/Experimenting-with-the-font-LEXEND/","permalink":"https://kiko.io/post/Experimenting-with-the-font-LEXEND/","excerpt":"A few weeks ago, a blog entry (I can’t remember which one) drew my attention to a font called LEXEND. In this article, the author also went into the scientific background of the font, which was developed to simplify reading and thus support those with reading difficulties. The website lexend.com, operated by The Lexend Group, therefore also advertises the font with all kinds of reading statistics, although it is open source and is also freely available via Google Fonts. I simply liked the font style and wanted to try it out on this blog, which has always used the Open Sans font. It was harder than I thought…","keywords":"weeks ago blog entry remember drew attention font called lexend article author scientific background developed simplify reading support difficulties website lexendcom operated group advertises kinds statistics open source freely google fonts simply style wanted sans harder thought…","text":"A few weeks ago, a blog entry (I can’t remember which one) drew my attention to a font called LEXEND. In this article, the author also went into the scientific background of the font, which was developed to simplify reading and thus support those with reading difficulties. The website lexend.com, operated by The Lexend Group, therefore also advertises the font with all kinds of reading statistics, although it is open source and is also freely available via Google Fonts. I simply liked the font style and wanted to try it out on this blog, which has always used the Open Sans font. It was harder than I thought… The Download OdysseyAt the bottom of the one-pager website lexend.com there is a form for the first name and e-mail address and a button “Subscribe & Send me these Fonts”. Okay … Seriously? Fuel for the spam machine? But my spam filter seems to be good enough and so I had the mail sent to me, only to be offered a file called readexpro-master.zip for download via the tracker button in the mail called “Download the Latest Fonts”. Again … Seriously? I expect the Lexend font files and I get something else, without any hint or description? How quickly do the people at The Lexend Group want to destroy their reputation on the net? Where was the UNSUBSCRIBE link in the mail again?But of course I could have used the less prominent link to Google Fonts below the sample image in the mail, which actually leads to the expected font. But what the heck is ReadExPro? Research… The font was designed in 2018 by Bonnie Shaver-Troup and Thomas Jockin and is based on the Quicksand project by Andrew Paglinawan, which was initiated in 2008 and improved by Thomas Jockin for Google Fonts in 2016. However, lexend.com only mentions Shaver-Troup and her team, who started working on Lexend with Google in 2017. In 2021, Thomas Jockin forked the project and worked with Nadine Chahine on an expansion for Arabic and renamed the font Readex Pro. Ah … ok. And why doesn’t anyone tell you that on the Lexend site? So Lexend is only one half of the coin and Readex Pro makes it a whole? Why two names for it? And why do you get one when you expect the other? Mysterious and, in times of all kinds of dangers on the Internet, simply bordering on the dubious. But unfortunately this is not the end of the inconsistencies. Since the files in my download of ReadEx Pro are quite large and I have no need for Arabic characters, I downloaded the Lexend font package from Google Fonts, which ONLY contains TTF files and no WOFF2 (Webfonts), the compressed version. Google itself writes the following in its own Glossary of Web Fonts, but then refrains from supplying them? Another: Seriously? WOFF (and its successor WOFF2) are compressed file formats created specifically for web fonts. Although regular OpenType fonts (TTF and OTF files) can be used as web fonts, such usage is not recommended as it usually contravenes licence agreements-and the files are significantly larger. However, if you embed the font directly from Google Fonts instead of downloading it, only WOFF2 is delivered and TTF is out of the picture. It may be a consistent approach by Google to offer as much as possible only online in order to be able to better analyse the traffic behind it, but especially in Germany, where a wave of warnings has swept over website operators in the last two years precisely because of this data collection via Google Fonts, this leaves a bitter aftertaste and forces developers to do additional work where it would not be necessary. Convert TTF to WOFF2A short detour … The Web Open Font Format (WOFF) has been around since 2012. The second version (WOFF2), in which the extremely efficient Brotli compression method is used, was drafted in 2014. All browsers support WOFF2 nowadays, so there is no longer any need to include WOFF, as is often still the case. The same applies to other historical and dead formats EOT and SVG. So if you have a font as TTF and want to use it on the web, you can either use the open source compression from Google on the command line or you can use a free online compressor such as the one from Yabe Webfonts. To include the font in your CSS, you ONLY need the following code for modern browsers. If you have to support old favorites like IE 8 or similar, you have completely different problems anyway and should think about changing jobs. 1234@font-face { font-family: 'MyWebFont'; src: url('myfont.woff2') format('woff2');} But back to Lexend and a download that works. All fonts on Google Fonts can also be found on GitHub and so there is also a repository for Lexend: googlefonts/lexend. In it you will find both TTF and WOFF2 files, at least for the standard font Lexend, although not for the variants (Deca, Lexa, Giga etc.), which I don’t need for my part anyway. 1234567891011121314151617181920212223242526\\lexend-main\\fonts\\lexend+---ttf Lexend-Black.ttf Lexend-Bold.ttf Lexend-ExtraBold.ttf Lexend-ExtraLight.ttf Lexend-Light.ttf Lexend-Medium.ttf Lexend-Regular.ttf Lexend-SemiBold.ttf Lexend-Thin.ttf +---variable Lexend[HEXP,wght].ttf +---webfonts Lexend-Black.woff2 Lexend-Bold.woff2 Lexend-ExtraBold.woff2 Lexend-ExtraLight.woff2 Lexend-Light.woff2 Lexend-Medium.woff2 Lexend-Regular.woff2 Lexend-SemiBold.woff2 Lexend-Thin.woff2 You might have noticed in the files list, that there are no italic or oblique font faces. There is a reason for this, because there are currently none for Lexend. Although there is a corresponding GitHub issue (Update Lexend with Italics), but it hasn’t really been going anywhere for over a year. Richard Hriech did publish a cursive variant called LexendItalic last year, but the project looks more like a quick fix created within a day and therefore I haven’t tried it out.However, the absence does not bother me, because according to the W3C, the browser mimics the so-called sloping effect if it cannot find an appropriate font. I think it does this quite well and saves me having to work with additional font files. The Integration MadnessLet’s get straight to the point: My difficulties in using Lexend here on kiko.io were entirely homemade. Integrating the font made me realize all the CSS mistakes I’ve made in recent years, because Lexend has a significantly different character spacing than OpenSans I’ve been using so far. It has never been a good idea to lean too heavily on typography when designing layouts. The careless mixing of REM and PX was also my downfall. Everything looked pretty shitty … and gave me the incentive to refactor the entire CSS (or Stylus), regardless of whether I would continue to use Lexend or not. After many days of standardising, dismantling, recalculating, rewriting and also throwing away, the site had a completely new and better look for me, although (mostly) nothing had changed in the basic structure. The Lexend font, which I find much more suitable, also contributed to this and so I will leave it like that. Whether you can now read my posts faster, as the font promises, is for others to judge. Toggle to Open Sans let bFontToggle = false; function fontToggle() { if (bFontToggle === false) { document.getElementById(\"body\").style.fontFamily = \"Open Sans\"; document.getElementById(\"body\").style.fontWeight = 400; document.getElementById(\"fontToggle\").textContent = \"Reset to Lexend\"; } else { window.location.reload(); } bFontToggle = !bFontToggle; } More Info lexend.com Lexend Italic Version How to Convert a TTF Variable Font to WOFF2 for Improved Web Performance How To Convert Variable TTF Font Files To WOFF2","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Theming","slug":"Theming","permalink":"https://kiko.io/tags/Theming/"}]},{"title":"SVWW vs. Hansa Rostock @ 2023-10-29","subtitle":"Another step to class retention","series":"SV Wehen Wiesbaden","date":"2023-10-30","updated":"2023-10-30","path":"post/SVWW-vs-Hansa-Rostock-2023-10-29/","permalink":"https://kiko.io/post/SVWW-vs-Hansa-Rostock-2023-10-29/","excerpt":"1:0 Football again. After my little excursion into the Europa Conference League, my team from SV Wehen Wiesbaden was called upon again on Sunday and with them the fans in the stadium. On the one hand because it was raining (and that is sometimes no fun in the front row) and on the other hand because the boys had won the last away game in Osnabrück and now had the opportunity to temporarily establish themselves in the midfield of the 2. Bundesliga. Another win was needed against an opponent from Rostock who, after a good start at the beginning of the season, had lost the last 6 games! Since I have no chance to park my car anywhere at the stadium, I have already planned before the season to always go there with the scooter, which is a pure pleasure in the summer. With 10 degrees and light rain it was diemal but rather suboptimal conditions. But anyway… I had expected that a lot of police, water cannon and other executive equipment would be on site, as it was already the case at the Schalke game, because the Hansa fans have a pretty bad reputation … But no. Not particularly many Rostockers have taken on the 700 kilometer journey and so the opposing stand was only half full and the police relaxed.","keywords":"football excursion europa conference league team sv wehen wiesbaden called sunday fans stadium hand raining fun front row boys won game osnabrück opportunity temporarily establish midfield bundesliga win needed opponent rostock good start beginning season lost games chance park car planned scooter pure pleasure summer degrees light rain diemal suboptimal conditions anyway… expected lot police water cannon executive equipment site case schalke hansa pretty bad reputation … rostockers kilometer journey opposing stand half full relaxed","text":"1:0 Football again. After my little excursion into the Europa Conference League, my team from SV Wehen Wiesbaden was called upon again on Sunday and with them the fans in the stadium. On the one hand because it was raining (and that is sometimes no fun in the front row) and on the other hand because the boys had won the last away game in Osnabrück and now had the opportunity to temporarily establish themselves in the midfield of the 2. Bundesliga. Another win was needed against an opponent from Rostock who, after a good start at the beginning of the season, had lost the last 6 games! Since I have no chance to park my car anywhere at the stadium, I have already planned before the season to always go there with the scooter, which is a pure pleasure in the summer. With 10 degrees and light rain it was diemal but rather suboptimal conditions. But anyway… I had expected that a lot of police, water cannon and other executive equipment would be on site, as it was already the case at the Schalke game, because the Hansa fans have a pretty bad reputation … But no. Not particularly many Rostockers have taken on the 700 kilometer journey and so the opposing stand was only half full and the police relaxed. The nice old lady to my left (I really have to ask her about her name) was also there again, after missing the last home game due to a wedding, as she told me. The silent one on my right also and also this time he didn’t speak a word, so I’m starting to wonder if he can talk at all. The howler monkeys behind me were of course also there again and they had in the course of the game again plenty of opportunity to insult the referee, opposing players or our coach in the worst way. This time there were only 8,600 spectators, which was probably also due to the weather. It was just cold and wet and I felt a bit sorry for the players, especially the goalkeepers. How do you keep warm when you’re standing on the field in shorts and not constantly running back and forth? A football mystery. The GameIn the first 30 minutes the game was quite even. You could tell that both teams wanted a win, but the goal attempts, especially from Rostock, were nothing more than puny. Ours did a little better, but the ball still always went past the goal. Both teams were also a bit aggressive, for example, Perea from Rostock annoyed our keeper Stritzel so much that he extended his elbow and Perea sank to the ground like a striving man and rolled back and forth theatrically. An undignified spectacle, which the fans acknowledged with shrill whistles … and the referee with a penalty! Stritzel has saved fantastic balls this season, but not a penalty yet … until then, as he parried Kinsombi’s weak shot to the side. Yesss…! Nothing much happened until half-time, but our guys were slowly gaining the upper hand, as the game was played almost exclusively in the opponent’s half at the beginning of the second half, with occasional counter-attacks by Rostock, but they were too erratic to have any effect. Our defence stood firm. let macy_1o3k41 = new Macy({ container: '#image-masonry-1o3k41', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); The increasing control of the game also led to more chances to score, but either it went just over or Rostock keeper Markus Kolke (who played a total of 8 years in Wiesbaden until 2019) fished it out of the corners. The closer we got to the end of the game, the more nervous the fans (or just me and my seatmate) got. OMG, that thing has to go in, now! The redemption came in the 89th minute: After a standard, Kolke parried Fechner’s shot forward and Prtajin only had to stick his head out … 1:0! ⇾ Match report on kicker.de ConclusionIt was a nerve-wracking, but in the end a happy game. We fans would like to see more of these games, especially if it ends in a win. I haven’t seen our boys lose since I started going to the stadium thsi season. We can keep it up :D","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"},{"name":"2. Bundesliga","slug":"2-Bundesliga","permalink":"https://kiko.io/tags/2-Bundesliga/"}]},{"title":"SGE vs. HJK @ 2023-10-26","subtitle":"Europa Conference League: Eintracht Frankfurt against HJK Helsinki","date":"2023-10-29","updated":"2023-10-29","path":"post/SGE-vs-HJK-2023-10-26/","permalink":"https://kiko.io/post/SGE-vs-HJK-2023-10-26/","excerpt":"6:0 My newly awakened love for German soccer reached a small peak last Thursday when my colleague Uli asked me, if I would like to go to the evening match of his favorite club Eintracht Frankfurt against HJK Helsinki as part of the preliminary round of the Europa Conference League. He had come by a happy coincidence to two tickets. Of course! Let’s go! Now I’m not an Eintracht fan, by any means. In the Rhine-Main area, the Frankfurt club is either hated or loved by the people, because its fan base is a bit more aggressive, i.e. they are disreputable and with them the club. But I’ve never been to the Waldstadion (as it used to be called) or the “Deutsche Bank Park” (as it’s called if you put money on the table), one of the big soccer arenas in Germany, where international matches and other big events are also held regularly.","keywords":"newly awakened love german soccer reached small peak thursday colleague uli asked evening match favorite club eintracht frankfurt hjk helsinki part preliminary round europa conference league happy coincidence tickets lets im fan means rhine-main area hated loved people base bit aggressive disreputable ive waldstadion called deutsche bank park put money table big arenas germany international matches events held regularly","text":"6:0 My newly awakened love for German soccer reached a small peak last Thursday when my colleague Uli asked me, if I would like to go to the evening match of his favorite club Eintracht Frankfurt against HJK Helsinki as part of the preliminary round of the Europa Conference League. He had come by a happy coincidence to two tickets. Of course! Let’s go! Now I’m not an Eintracht fan, by any means. In the Rhine-Main area, the Frankfurt club is either hated or loved by the people, because its fan base is a bit more aggressive, i.e. they are disreputable and with them the club. But I’ve never been to the Waldstadion (as it used to be called) or the “Deutsche Bank Park” (as it’s called if you put money on the table), one of the big soccer arenas in Germany, where international matches and other big events are also held regularly. Getting to the stadium was an ordeal, because we had left our cars at the office and taken the cab, but the 55,500 spectators all seemed to want to park right in front of the arena so the traffic jam was immense. The last kilometer through the forest to the stadium and the Jürgen Grabowski grandstand to our seats were tough, at least for Uli, because his knee was hurting as usual. But the anticipation, a beer and an Äppler made up for everything. What I found amazing, as a Waldstadion newbie, was that we entered the building at ground level, but came out in row 30. The whole thing thus reminded of a nicely designed giant pit. The lighting of this massive area was perfect and the volume of the fans was loud, but not deafening. An architectural masterpiece. The wall of fans in the home curve with the many oversized flags that were waved throughout was impressive, even if I wondered the whole time, how the fans behind these giant flags would have noticed anything of the game for even a minute. But I think it was not about free view of the people there at all. Only about the atmosphere. I was in any case happy about my seat in row 9 pretty directly on the center line of the field and had to ask the people in the row of seats in front of me once in a while to sit down again so that I could see the game comfortably. … vs … (cute) The GameThe first 10 minutes of the game were unspectacular. They felt each other out and the Finns played diligently and should have been in the lead after 5 minutes, but got in the 9th minute after some guesswork from the referee with VAR support a penalty against them. let macy_oz7qh4 = new Macy({ container: '#image-masonry-oz7qh4', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); After this 1:0 for Eintracht, the Frankfurt boys whirled themselves into a kind of frenzy of play. More and more fluidly and casually it went in the direction of the Finnish goal and Helsinki could only with effort hold against it. So it was after 30 minutes already 3:0 and at halftime 4:0. The goal to make it 3-0 was a really spectacular one: Omar Marmoush gets the ball in the penalty area and takes a shot, but the ball bounces off an opponent’s legs and comes back to him. He pulls off again and again the ball bounces off the legs of another player back to him. His third attempt was then a fine shot with the outside of his foot past all the players into the goal. In the break, the coach of the Finns seems to have been a little louder, because his guys played a little stronger and more determined, only to get another one in the 55th minute. With 10 minutes to go, things got emotional in the stadium: the coach substituted Timothy Chandler, a Frankfurt veteran who has been with the club for almost 10 years and his first appearance this season. The tens of thousands of fans were completely out of their minds, screaming “Tiiiimmmyyy” as soon as he got to the ball. It was crazy. How must it feel to be showered with so much fan love! Things almost got out of hand when Chandler sprinted forward on the right flank in the 89th minute and made a wonderful pass into the middle and Dina Ebimbe just had to slot it in for 6:0. let macy_whk9da = new Macy({ container: '#image-masonry-whk9da', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); ⇾ Match report on kicker.de ConclusionIt was really an experience to see this game in this arena and even if I would have enjoyed Helsinki sending Frankfurt home 4:0 (my funny tip to Uli before the game), it was nice that he could be happy about the victory.","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"Eintracht","slug":"Eintracht","permalink":"https://kiko.io/tags/Eintracht/"}]},{"title":"Handling IPTC metadata on Android and Windows","subtitle":null,"date":"2023-10-28","updated":"2023-10-28","path":"post/Handling-IPTC-metadata-on-Android-and-Windows/","permalink":"https://kiko.io/post/Handling-IPTC-metadata-on-Android-and-Windows/","excerpt":"Most of the photos that I prepare for this website for example, I take with my big Nikon camera in RAW format and then edit them under Windows in Adobe Lightroom Classic. I always give the image a name and a few keywords, which are then also displayed on the photo page. Lightroom stores these as IPTC metadata in the sidecar file and writes them, when exporting the image, directly into the header of the generated JPG file. Now, however, I take pictures on the go with my Pixel smartphone and process them with the Google Photos Android app, and although the IPTC standard is only 30 years old, Google doesn’t support it! You can give the image a “description” but it is not written into the image as a metadata, but is probably lying around somewhere on the Google servers.","keywords":"photos prepare website big nikon camera raw format edit windows adobe lightroom classic give image keywords displayed photo page stores iptc metadata sidecar file writes exporting directly header generated jpg pictures pixel smartphone process google android app standard years doesnt support description written lying servers","text":"Most of the photos that I prepare for this website for example, I take with my big Nikon camera in RAW format and then edit them under Windows in Adobe Lightroom Classic. I always give the image a name and a few keywords, which are then also displayed on the photo page. Lightroom stores these as IPTC metadata in the sidecar file and writes them, when exporting the image, directly into the header of the generated JPG file. Now, however, I take pictures on the go with my Pixel smartphone and process them with the Google Photos Android app, and although the IPTC standard is only 30 years old, Google doesn’t support it! You can give the image a “description” but it is not written into the image as a metadata, but is probably lying around somewhere on the Google servers. Downloading the image via Google Photos and giving it a name and a few keywords using on-board tools under Windows doesn’t work either. There is the display of metadata in File Explorer under PROPERTIES and DETAILS, but editing TITLE or DESCRIPTION does not lead to IPTC metadata either, because these are not supported by Microsoft. Now you can ask yourself why IPTC is not important to the big companies, especially since it doesn’t seem to be the problem technically, as Adobe and other companies prove, but so what … I need a solution for my problem. Android SolutionFor Android, there is probably no better image metadata app than EXIF Pro. Besides the GALLERY, which lists all images on the smartphone, it also has a browser to load other images and it supports all metadata formats, including IPTC. All metadata can be edited, added or deleted if you want to save space, for example. In my case, I just have to select the image in the app and use ADD in the IPTC section to select the ObjectName (for the name) and Keywords (for the keywords), enter the values and save the file. Done. The app is based on the platform-independent Perl library ExifTool by Phil Harvey … and as often as you research for ways to manipulate image metadata and whatever platform on the net, you will always come across Phil’s tool. It is the gold standard in this area, so to speak. The list of supported formats is more than impressive and that of meta formats leaves nothing to be desired. Windows SolutionMy problem described above is actually already solved with the Android solution, but under Windows there is this IPTC gap too and it can be filled with … what do you think … the ExifTool from Phil Harvey ;) First of all, ExifTool is a pure command line tool and also comes without an installer or anything like that. For Windows there is a ZIP download with a single file inside: exiftool(-k).exe. You save it in a folder of your choice and drag & drop an image file onto it and the metadata will be displayed in a terminal window. “-k” in the name means that ExifTool is called with this parameter, which makes sure that the terminal window stays open after processing. For use in the command line it is best to copy the file and rename it to exiftool.exe or you can download the installer variant by Oliver Betz, which also makes a PATH entry to be able to call it from anywhere. To add the name and the keywords to a file, the following call is sufficient: (lines wrapped for readability)12345exiftool -overwrite_original -iptc:ObjectName="My New Name" -iptc:Keywords="keyword1,keyword2,keyword3" "C:\\Pictures\\MyImage.jpg" Okay … You don’t buy Windows to use the command line. There is also a GUI called ExifToolGUI for Windows v5.xx by Bogdan Hrastnik. It is already more than 10 years old and also comes without an installer, but it works great … if you have understood the somewhat strange concept of workspaces and adding new fields to them, or if you have somehow muddled your way through. ConclusionI wish IPTC would not only be supported by the handful of programs, but like the technical meta format EXIF would become a standard in every image processing software, especially in the Big Five, who neglect it so far. But maybe my contribution here helped you to deal with it.","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Android","slug":"Android","permalink":"https://kiko.io/tags/Android/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"},{"name":"Metadata","slug":"Metadata","permalink":"https://kiko.io/tags/Metadata/"}]},{"title":"Get and use a dominant color that matches the header image","subtitle":"Vibrant Color library and recalculating its HSL output values","date":"2023-10-10","updated":"2023-10-10","path":"post/Get-and-use-a-dominant-color-that-matches-the-header-image/","permalink":"https://kiko.io/post/Get-and-use-a-dominant-color-that-matches-the-header-image/","excerpt":"The pages of this website are all structured according to the scheme Header, Content, Footer, where the header contains an individual hero image and, like the footer, has a dark gray background color. I chose dark gray at the time because I needed contrast and it matches all the other colors, since the header image is slowly overlaid by the header background color as you scroll. In July last year, in Discoveries #19 - Visual Helpers, I introduced two tools that deal with determining the dominant color from an image, and since then I’ve been buzzing around in my head about using one of them to color the header and footer to match the image. After a short testing period I decided to use Vibrant Colors (also because of the good example by Konstantin Polunin on Codepen) and how it works and how I had to adapt the results to my needs I want to highlight in this post.","keywords":"pages website structured scheme header content footer individual hero image dark gray background color chose time needed contrast matches colors slowly overlaid scroll july year discoveries #19 - visual helpers introduced tools deal determining dominant ive buzzing head match short testing period decided vibrant good konstantin polunin codepen works adapt results highlight post","text":"The pages of this website are all structured according to the scheme Header, Content, Footer, where the header contains an individual hero image and, like the footer, has a dark gray background color. I chose dark gray at the time because I needed contrast and it matches all the other colors, since the header image is slowly overlaid by the header background color as you scroll. In July last year, in Discoveries #19 - Visual Helpers, I introduced two tools that deal with determining the dominant color from an image, and since then I’ve been buzzing around in my head about using one of them to color the header and footer to match the image. After a short testing period I decided to use Vibrant Colors (also because of the good example by Konstantin Polunin on Codepen) and how it works and how I had to adapt the results to my needs I want to highlight in this post. The work on the project node-vibrant, is probably based on vibrant.js (at least this archived repo refers to it) and is actually a Node.js solution, but you can also just use it in the browser. Since the determination of the total of 6 different colors (Vibrant, Muted and both with its variants of Light und Dark mode) from an image can possibly lead to performance problems in the browser, the maintainers have focused on Node.js and also built in a WebWorker solution. My approach is based on the already fully loaded page, including the header image and the display with the original dark gray background color. So the new color is simply icing and thus should not lead to problems. Here is an example of the 6 extracted colors with one of my images from Konstantine’s codepen, mentioned above: SetupThe installation is done quickly … Execute npm install node-vibrant in the console, copy out the file vibrant.min.js into the distribution script folder and include it into the HTML page header: 1<script src="/js/dist/vibrant.min.js"></script> PreparationDepending on the size of the image to be read out, the determination may take some time. Therefore, it is advisable, as in my case, not only to compress the images, but also to let them preload in the head of the page. For this you can use the rel=preload mechanism, which ensures that a resource is loaded fast. The preload value of the element’s rel attribute lets you declare fetch requests in the HTML’s , specifying resources that your page will need very soon, which you want to start loading early in the page lifecycle, before browsers’ main rendering machinery kicks in. --- MDN Web Docs By adding an ID to the LINK it will be easy later on to get the URL of the image to throw at the library: 1234<link rel="preload" as="image" id="photo-preload" href="/photos/normal/20-08-Mallorca-7333.jpg" imagesrcset="/photos/mobile/20-08-Mallorca-7333.jpg 480w, /photos/tablet/20-08-Mallorca-7333.jpg 768w"> Usage, First AttemptThus prepared, it is easy to read out the preferred RGB color value in a script that is embedded in the footer of the page, using Vibrant Color: after-footer.js12345678910111213141516171819202122232425function setVibrantColor() { // get url of preloaded image let img = $("#photo-preload").attr("href"); // call Vibrant Color Vibrant.from(img).getPalette().then(palette => { // get vibrant color swatch let swatch = palette.Vibrant; // helper to format output const _rgbToString = (_rgb) => `rgb(${_rgb[0]}, ${_rgb[1]}, ${_rgb[2]})`; // get formatted RGB value of swatch let rgb = _rgbToString(swatch.getRgb()); // set new background color to header and footer $("#header, #footer").css("background-color", rgb); });}$(document).ready(function() { setVibrantColor();}); The MUTED variants were out of the question for me after a bit of trial and error, because for me this color has too little to do with what the eye immediately draws out of the image. This can be seen in the example above: the green of the MUTED colors is by no means dominant. That’s why I decided to use VIBRANT. Usage, Second AttemptBut there is also a problem with the VIBRANT variant: under certain circumstances the extracted color is too bright for the background with white text in the foreground. Vibrant Color offers the possibility to create matching text colors, but neither worked for me, nor did I want to get away from white. If you additionally offer a dark theme like I do, the selected color will definitely be too light. Besides the possibility to read out the RGB color from the so-called swatches, the library also offers the output HSL (Hue/Saturation/Lightness). By adjusting the L-value downwards, the desired contrast can be achieved again. The HSL values in Vibrant Colors are only available as decimal numbers, i.e. it is necessary to convert them using the HSL specification: after-footer.js12345678910111213141516171819202122232425262728293031function setVibrantColor(theme) { // get url of preloaded image let img = $("#photo-preload").attr("href"); // call Vibrant Color Vibrant.from(img).getPalette().then(palette => { // get vibrant color swatch let swatch = palette.Vibrant; // get HSL from swatch and convert values let _hsl = swatch.getHsl(); let h = (_hsl[0] * 360); // in relation to the HSL color wheel let s = (_hsl[1] * 100); // percentage let l = (_hsl[2] * 100); // percentage // overwrite l with new fixed hue depending on theme l = (theme === "dark") ? 20 : 35; // helper to format output, limited to 4 decimal places const _hslToString = (h, s, l) => `hsl(${h.toFixed(4)}, ${s.toFixed(4)}%, ${l.toFixed(4)}%)`; // get formatted HSL value let hsl = _hslToString(h, s, l); // set new background color to header and footer $("#header, #footer").css("background-color", hsl); });} Usage, Final AttemptHSL generally has the problem that colors appear brighter than others depending on the saturation. Therefore, the HSV color model is often preferred, because it corresponds more closely to human color perception. A complete conversion to HSV seemed a bit too much for my case. Therefore, I limited myself to downscaling the lightness proportionally from a saturation value of 70%: after-footer.js1234567891011121314function setVibrantColor(theme) { ... // overwrite l with new fixed hue depending on theme l = (theme === "dark") ? 20 : 35; // calculate hue reduction when saturation too high let reduce = (s > 70) ? 10 - ((100 - s) / 4) : 0; // take half of reduction when dark theme l -= (theme === "dark") ? reduce / 2 : reduce; ...} The ResultThe result convinces me, even if it is not (yet) perfect. But that is primarily due to the choice of the dominant color of the library. According to my color perception, it could be different from time to time … like for example for the page of this post. My eye would have expected some kind of violet. But on other pages it works fine, even the reduced lightness, because my header image at the top is darkened too with a gradient to let the text pop out more.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"},{"name":"Theming","slug":"Theming","permalink":"https://kiko.io/tags/Theming/"}]},{"title":"SVWW vs. HSV @ 2023-10-07","subtitle":"Two hearts beat in my chest","series":"SV Wehen Wiesbaden","date":"2023-10-07","updated":"2023-10-07","path":"post/SVWW-vs-HSV-2023-10-07/","permalink":"https://kiko.io/post/SVWW-vs-HSV-2023-10-07/","excerpt":"1:1 Since my youth I am a (small) fan of the Hamburger Sportverein (HSV). Magath, Hrubesch, Keegan were my sporting heroes back then. This was certainly also related to the fact that the club was one of the best in Europe in the early 80s. Two championships, European Cup victory and a strong team left an impression on me. As the last founding member of the 1. Bundesliga, however, it was always in danger of relegation in the last 10 years and then really relegated to the 2. Bundesliga in 2018 after 55 years. What followed were sad attempts to climb back up. Always so close that the saying prevailed: “How can you tell that it’s springtime in Germany? The trees are sprouting and HSV is fucking up the promotion!” In my first post in this series, I described how my hometown club from Wiesbaden made it into the 2. Bundeliga and this particular home game this weekend has a special appeal for me, of course. The favorite club from my youth against the one from my present. My Wehen Wiesbaden against my HSV.","keywords":"youth small fan hamburger sportverein hsv magath hrubesch keegan sporting heroes back related fact club europe early 80s championships european cup victory strong team left impression founding member bundesliga danger relegation years relegated sad attempts climb close prevailed springtime germany trees sprouting fucking promotion post series hometown wiesbaden made bundeliga home game weekend special appeal favorite present wehen","text":"1:1 Since my youth I am a (small) fan of the Hamburger Sportverein (HSV). Magath, Hrubesch, Keegan were my sporting heroes back then. This was certainly also related to the fact that the club was one of the best in Europe in the early 80s. Two championships, European Cup victory and a strong team left an impression on me. As the last founding member of the 1. Bundesliga, however, it was always in danger of relegation in the last 10 years and then really relegated to the 2. Bundesliga in 2018 after 55 years. What followed were sad attempts to climb back up. Always so close that the saying prevailed: “How can you tell that it’s springtime in Germany? The trees are sprouting and HSV is fucking up the promotion!” In my first post in this series, I described how my hometown club from Wiesbaden made it into the 2. Bundeliga and this particular home game this weekend has a special appeal for me, of course. The favorite club from my youth against the one from my present. My Wehen Wiesbaden against my HSV. Unfortunately I missed the game two weeks ago against Elversberg, because I had already made a promise to go to Mannheim to the Bundesgartenschau, but maybe it was a good thing, because my team was in a bad mood and lost 0:2. Just like the last away game against Hannover (2:0) and the cup game against RB Leipzig at home in between (2:3), where I actually wanted to go, but due to a small error in understanding did not get into the stadium. So it was time to go back to the arena, because with me SVWW always had something to cheer about, even if it was just a draw that felt like a win. I also have a few HSV fans in my circle of friends and so I made use of my right of first refusal of 4 more tickets for this game. I sat on my permanent seat in the front row and my friends at a few rows behind me. The elderly lady on my left was not present this time, but a nice man who had taken her place was. The silent one on my right did not come at all and his seat thus remained empty. The GameHSV is actually in shape this year to make it back to the Bundesliga. They have already dropped a few points, but have been on the promotion places from the beginning. This year it should be something and accordingly dominant have the Hamburg the game against once again deep standing Wiesbadener also started. Very sure of the ball and winning almost every duel, they were unable to capitalize on this in the first half. They brought the ball very close to the goal, but not into it, and you could tell that the guests were getting a little more annoyed as time went on. This was expressed shortly before the half-time break also in a few unsportsmanlike conduct. Dropping and claiming foul play is simply stupid and only provokes catcalls. The one or other time is overall bad referee but also fell for the trick or has made other nonsensical decisions, which caused the “right” fans around me to nastiest insults. let macy_zs6k4c = new Macy({ container: '#image-masonry-zs6k4c', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); After the break, the game continued just as it had stopped a quarter of an hour earlier: HSV played beautifully, safely but ineffectively. They even scored a goal, but it was disallowed for offside. The SVWW was defending with all men in their own half, sometimes desperately, and only rarely managed to counterattack. 9 minutes before the end, the Wiesbadener were then once in front of the opposing goal and from a rather harmless header and a weak defense of the goalkeeper became a goal! 1:0! Unbelievable! The remaining 9 minutes plus 6 minutes of injury time can be called spectacular: HSV ran frantically and tried everything to put the f***ing ball in the goal and in the 87th minute the ball was actually in our net … 1:1! But that was not enough for the guests. They kept increasing the pressure to win this game. Partly too hectic and headless, but we had real trouble to fend them off. However, one of these defensive actions in the penalty area led to a penalty kick for HSV in the 97th minute. Damn! No! … but the penalty taker slammed the thing against the crossbar! YESSS! ⇾ Match report on kicker.de ConclusionAnother one of those draws that feels like a win. It’s a pity that it wasn’t enough for a win, but you have to admit that HSV was really a class above. They’re really playing for promotion and we’re playing to stay in the league. What gives me courage is that the Wiesbaden team fights so passionately and never gives up. Every opponent has a very hard time with this defense and that’s a good approach to still play in the 2nd Bundesliga next year.","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"},{"name":"2. Bundesliga","slug":"2-Bundesliga","permalink":"https://kiko.io/tags/2-Bundesliga/"}]},{"title":"Majorcan Details","subtitle":null,"series":"New Photos","date":"2023-10-03","updated":"2023-10-03","path":"post/Majorcan-Details/","permalink":"https://kiko.io/post/Majorcan-Details/","excerpt":"Summer is over and I’m spending some of my free time post-processing in Lightroom the many photos I’ve taken over the past few months. As a casual photographer, I mostly use my vacation and weekend trips to take my Nikon for a walk and let its chip card glow. My better half is constantly wondering why I stopped there or here again, even if she likes the results afterwards. The point is that I am a color-and-shape junkie. Of course I also do landscapes and portraits, but the details of things have done it to me the most. Colors & Shapes … In July I was for a few days with friends on a finca in the south of Mallorca and of course I used every opportunity to stick my lens everywhere where there were interesting details to be expected. I don’t know if it had anything to do with the heat, but most of the motifs have a very earthy touch. I probably wanted to stay somewhere underground all the time, because 40 degrees Celsius in the shade was then also a bit too much for me.","keywords":"summer im spending free time post-processing lightroom photos ive past months casual photographer vacation weekend trips nikon walk chip card glow half constantly wondering stopped likes results point color-and-shape junkie landscapes portraits details things colors & shapes … july days friends finca south mallorca opportunity stick lens interesting expected dont heat motifs earthy touch wanted stay underground degrees celsius shade bit","text":"Summer is over and I’m spending some of my free time post-processing in Lightroom the many photos I’ve taken over the past few months. As a casual photographer, I mostly use my vacation and weekend trips to take my Nikon for a walk and let its chip card glow. My better half is constantly wondering why I stopped there or here again, even if she likes the results afterwards. The point is that I am a color-and-shape junkie. Of course I also do landscapes and portraits, but the details of things have done it to me the most. Colors & Shapes … In July I was for a few days with friends on a finca in the south of Mallorca and of course I used every opportunity to stick my lens everywhere where there were interesting details to be expected. I don’t know if it had anything to do with the heat, but most of the motifs have a very earthy touch. I probably wanted to stay somewhere underground all the time, because 40 degrees Celsius in the shade was then also a bit too much for me. Usually the best photos end up unnoticed as pool photos here on my blog until I use one as a header image, but now I make a separate post out of each set before I publish the individual photos on other sites like 500px or Pixelfed. Lots of Things Mallorquin Letterbox Basket Parking Aigua Tub Tomato Beauties Veltins Crown Rusty Grate Sticker Direction Table Greenery","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"Mastodon Share Bottom Sheet Dialog","subtitle":"Implement a 'Share Post' dialog in Hexo","date":"2023-09-28","updated":"2023-09-28","path":"post/Mastodon-Share-Bottom-Sheet-Dialog/","permalink":"https://kiko.io/post/Mastodon-Share-Bottom-Sheet-Dialog/","excerpt":"Social media thrives on sharing. Thoughts, experiences, self-dramatizations or even texts or posts from other people. This is especially true for microblogging on (formerly) Twitter and (today) Mastodon. To make this easy, the platforms often offer a SHARE endpoint (URL), such as the famous http://www.twitter.com/share?text=My impossible thoughts on X … or any dirt buttons to collect user data. A few months after the groundbreaking of Mastodon 2016, such a feature was also discussed and implemented on GitHub and out came: https://<your-instance>/share&text=My benevolent thoughts on the Fediverse But … where Twitter had it easy due to its central structure (twitter.com … period.), we Mastodon users all fidget around on different instances, i.e. each instance has its own /SHARE endpoint and so it’s a bit harder to stick a share button on your own blog, because you have to ask the user where he lives. Of course, online services like toot.kytta.dev, s2f.kytta.dev, mastodonshare.com sprouted immediately, but also the button providers expanded their portfolio or new ones were launched, like shareon.js.org, share-on-mastodon.social, shareaholic.com. But seriously … does it take an external service to ask the user for an instance name and redirect him to an URL (and run the risk of falling victim to data collection mania)? Because that’s all it is. All of the above do it exactly that way. So we come to the ready-made developer solutions e.g. How-To tutorials. Here, too, there are a lot of hits after a search: Mastodon-share-button (WebComponent), Mastodon-share-button (JS), Adding a Share On Mastodon button to a website, Adding a “share to mastodon” link to any web site – and here, … and I’ll join them here for my Hexo-driven blog.","keywords":"social media thrives sharing thoughts experiences self-dramatizations texts posts people true microblogging twitter today mastodon make easy platforms offer share endpoint url famous http://www.twitter.com/share?text=My impossible … dirt buttons collect user data months groundbreaking feature discussed implemented github https://<your-instance>/share&text=My benevolent fediverse due central structure twittercom period users fidget instances instance /share bit harder stick button blog lives online services tootkyttadev s2fkyttadev mastodonsharecom sprouted immediately providers expanded portfolio launched shareonjsorg share-on-mastodonsocial shareaholiccom external service redirect run risk falling victim collection mania ready-made developer solutions how-to tutorials lot hits search mastodon-share-button webcomponent js adding website link web site – ill join hexo-driven","text":"Social media thrives on sharing. Thoughts, experiences, self-dramatizations or even texts or posts from other people. This is especially true for microblogging on (formerly) Twitter and (today) Mastodon. To make this easy, the platforms often offer a SHARE endpoint (URL), such as the famous http://www.twitter.com/share?text=My impossible thoughts on X … or any dirt buttons to collect user data. A few months after the groundbreaking of Mastodon 2016, such a feature was also discussed and implemented on GitHub and out came: https://<your-instance>/share&text=My benevolent thoughts on the Fediverse But … where Twitter had it easy due to its central structure (twitter.com … period.), we Mastodon users all fidget around on different instances, i.e. each instance has its own /SHARE endpoint and so it’s a bit harder to stick a share button on your own blog, because you have to ask the user where he lives. Of course, online services like toot.kytta.dev, s2f.kytta.dev, mastodonshare.com sprouted immediately, but also the button providers expanded their portfolio or new ones were launched, like shareon.js.org, share-on-mastodon.social, shareaholic.com. But seriously … does it take an external service to ask the user for an instance name and redirect him to an URL (and run the risk of falling victim to data collection mania)? Because that’s all it is. All of the above do it exactly that way. So we come to the ready-made developer solutions e.g. How-To tutorials. Here, too, there are a lot of hits after a search: Mastodon-share-button (WebComponent), Mastodon-share-button (JS), Adding a Share On Mastodon button to a website, Adding a “share to mastodon” link to any web site – and here, … and I’ll join them here for my Hexo-driven blog. My Mastodon Share VariantIt’s really not hard to develop an appealing looking and working JavaScript solution to the “problem”. Where Christian Heilmann (last link in the list above) relies on a simple window.prompt, I recently introduced my Bottom Sheet dialogs in this blog (here and here), which are perfectly suited for this. Each shareable post should get a SHARE button that lets a bottom sheet pop out where the user can enter their instance name, which is then remembered for next time, and already contains suggested text (title, description, and url of the post) for the Mastodon toot. The ButtonIn every post the interaction section is rendered and there is now a new button, which calls a new dialog method, described later on: themes\\landscape\\layout\\_partial\\post\\interaction.ejs123...<button onclick="dialog.shareOnMastodon();" class="share mastodon">Share on Mastodon</button>... Adding a dialog template to the DOMFor my dialog I first needed a UI, preferably a template in the DOM to avoid having to fiddle with HTML in the JS code: themes\\landscape\\layout\\_partial\\templates\\mastodon-share-dialog.ejs12345678910111213141516171819<template id="mastodon-share-dialog"> <div class="mastodon-share-content"> <section> <p id="mastodon-share-intro"> <img src="/images/mastodon.svg" alt="Mastodon"> There are many Mastodon instances out there. Tell me yours and I will redirect you to the share dialog of your server: </p> <label for="mastodon-instance">Your Mastodon Instance</label> <div id="mastodon-instance-wrapper"> <span>https://</span> <input id="mastodon-instance" name="intance" required /> </div> <label for="mastodon-text">Text to share ...</label> <textarea id="mastodon-text" name="text" rows="8"></textarea> <button id="mastodon-share">Share</button> </section> </div></template> The template is embedded in layout.ejs near the closing HTML tag. It contains mainly an INPUT for the instance name, a TEXTAREA for the suggested text and a button for the action. I’ll leave out the styles here, because if you adopt my implementation, the CSS (or Stylus) will be completely different. Extending the dialog scriptOk … let’s extend the dialog.js code, I first described here, with a new method as used at the button: themes\\landscape\\source\\js\\dialog.js123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960var dpDialog = { 'base': { ... }, 'init': function(options) { ... }, 'show': function(callback) { ... } 'shareOnMastodon': function() { dpDialog.base.init({ headerText: "Share on Mastodon", urlHash: "share", // show hash in url minContentHeight: 400, // limit height of dialog width: "min(600px, 100%)" // center dialog with max width 600px }); } // Grab the template and make it a jQuery object let content = document.getElementById("mastodon-share-dialog").content.cloneNode(true); let jContent = $(content); // Read the cookie if the user has already used the dialog ... let instance = getCookie("mastodon-instance"); if (instance) { // ... and enter it in the INPUT jContent.find("#mastodon-instance").val(instance); } // Create a suggestion text from the page metadata const title = document.querySelector('meta[name="title"]').content; const description = document.querySelector('meta[name="description"]').content; const permalink = document.querySelector('link[rel="canonical"]').href; jContent.find("#mastodon-text").val(title + "\\n\\n" + description + "\\n\\n" + permalink); // Set the BUTTON action ... jContent.find("#mastodon-share").click(function(e) { const eInstance = document.getElementById("mastodon-instance"); const eText = document.getElementById("mastodon-text"); // Let the browser validate the input attribute REQUIRED const isValid = eInstance.reportValidity(); if (isValid) { // Save the entered instance name to a cookie setCookie("mastodon-instance", eInstance.value); // Generate the share URL for the instance let shareUrl = `https://${eInstance.value}/share?text=${encodeURIComponent(eText.value)}`; // Open the share URL in a new window and close the dialog window.open(shareUrl, '_blank'); dpDialog.base.element.downupPopup("close"); } }); // Add the dialog to the DOM jContent.appendTo(dpDialog.base.content); // Open the dialog and set the focus in the INPUT dpDialog.base.show(function() { document.getElementById("mastodon-instance").focus(); });} The ResultThis is it … and here you can see the result of my implementation: … or look at the button in the bottom right corner and use it ;) As I said, it’s just a tool to make it easier for the user to share your content via a URL of their instance to their audience. There is no external stuff, data collection or influence on what is shared. Just JavaScript, HTML running in the browser and one cookie for convenience. You can find all files as usual on the GitHub repo of this blog at https://github.com/kristofzerbe/kiko.io","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Social Media","slug":"Social-Media","permalink":"https://kiko.io/tags/Social-Media/"},{"name":"Mastodon","slug":"Mastodon","permalink":"https://kiko.io/tags/Mastodon/"},{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/tags/JavaScript/"}]},{"title":"Contribute with Conventional Commits","subtitle":"Fun with a Pull Request","date":"2023-09-15","updated":"2023-09-15","path":"post/Contribute-with-Conventional-Commits/","permalink":"https://kiko.io/post/Contribute-with-Conventional-Commits/","excerpt":"I’ll be honest … I need some Git training. From time to time I contribute small things to GitHub projects and sometimes get confused with all the commands. Fork, Clone, Commit, Stage, Pull Request … all things that mean something to me, but that I certainly haven’t internalized. And so it happens that I sometimes mess up a pull request or something similar. Sure, my blog here also lives in GitHub, both in terms of source control and hosting on GitHub Pages, but here I’m the only one committing. No issues, no branches, no pull requests or anything else. I change something, hit commit and I’m done. Another point I can’t dismiss: I’m a Windows guy who likes to click buttons. The command line is not for me at all.What was the name of the parameter? Do I have to write --param=xxx or /param:xxx … damn where is the button?My brain is probably too small for that. Visual Studio Code is a big help there … it has buttons! But that doesn’t save me when it comes to Git, because you have to know in which order to press which of these buttons!","keywords":"ill honest … git training time contribute small things github projects confused commands fork clone commit stage pull request havent internalized mess similar blog lives terms source control hosting pages im committing issues branches requests change hit point dismiss windows guy likes click buttons command line allwhat parameter write --param=xxx /paramxxx damn buttonmy brain visual studio code big doesnt save order press","text":"I’ll be honest … I need some Git training. From time to time I contribute small things to GitHub projects and sometimes get confused with all the commands. Fork, Clone, Commit, Stage, Pull Request … all things that mean something to me, but that I certainly haven’t internalized. And so it happens that I sometimes mess up a pull request or something similar. Sure, my blog here also lives in GitHub, both in terms of source control and hosting on GitHub Pages, but here I’m the only one committing. No issues, no branches, no pull requests or anything else. I change something, hit commit and I’m done. Another point I can’t dismiss: I’m a Windows guy who likes to click buttons. The command line is not for me at all.What was the name of the parameter? Do I have to write --param=xxx or /param:xxx … damn where is the button?My brain is probably too small for that. Visual Studio Code is a big help there … it has buttons! But that doesn’t save me when it comes to Git, because you have to know in which order to press which of these buttons! Yesterday I discovered a small bug in my favorite Mastodon client Elk, which could be solved with one line of CSS. So … cloned the thing, looked for the source file, inserted the line … and googled how to submit the change on my repo clone as a pull request to the Elk project, and found this great tutorial on Medium from someone, with the name Supritha: >>> How to Create a Pull Request on GitHub using VS Code Fantastic. The pull request was a breeze. BUT … on the pull request page of Elk on GitHub the first check threw an error regarding Semantic Pull Request, with the message the title was wrong … what the heck? While I was reading the documentation (after the first attempt to change the title to something reasonable and another failure), one of the Elk developers Joaquín was so nice and corrected the title and pointed me to the very doc I was reading: >>> Conventional Commits <<< Okay … I was already happy that nothing went wrong this time, but that was too early, as so often in IT ;) Here to read: fix(ui): Empty lines in posts from Pixelfed are doubled/tripled (#2392) Since I am a structured person, I liked the commit message rules used by the Elk team, because they open up a space to make the commits easier to evaluate automatically afterwards. I think a great help for projects of this scale … currently 202 contributors! Actually I don’t have a Git project where the Convertional Commits would be useful, but for my future Me … READ THIS POST! (And for my employees, in case they read this: I have an idea that we should definitely establish soon … ;)","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Git/GitHub","slug":"Git-GitHub","permalink":"https://kiko.io/tags/Git-GitHub/"}]},{"title":"Discoveries #26 - JavaScript HowTo's","subtitle":null,"series":"Discoveries","date":"2023-09-12","updated":"2023-09-12","path":"post/Discoveries-26-JavaScript-HowTo-s/","permalink":"https://kiko.io/post/Discoveries-26-JavaScript-HowTo-s/","excerpt":"Over time, everyone accumulates links somewhere about procedures that one has not yet internalized. This is also the case with me and JavaScript development. “Damn … what was that about Call | Proxy | Map | <You name it>?!”. Here are 11 of them … Processing images with sharp in Node.jsUse console.log() like a pro!Simple Swipe with Vanilla JavaScriptThe File System Access API: simplifying access to local filesAn introduction to WebAssembly for JavaScript DevelopersGetting Started with the Map and Set Typed CollectionsJavaScript Currying: A Practical ExampleHow to Use the Call, Apply, and Bind Functions in JavaScriptHow JavaScript's Proxy Object WorksJavaScript waitFor PollingHow to measure page loading time with Performance API","keywords":"time accumulates links procedures internalized case javascript development damn … call | proxy map <you it> processing images sharp nodejsuse consolelog prosimple swipe vanilla javascriptthe file system access api simplifying local filesan introduction webassembly developersgetting started set typed collectionsjavascript currying practical examplehow apply bind functions javascripthow javascript's object worksjavascript waitfor pollinghow measure page loading performance","text":"Over time, everyone accumulates links somewhere about procedures that one has not yet internalized. This is also the case with me and JavaScript development. “Damn … what was that about Call | Proxy | Map | <You name it>?!”. Here are 11 of them … Processing images with sharp in Node.jsUse console.log() like a pro!Simple Swipe with Vanilla JavaScriptThe File System Access API: simplifying access to local filesAn introduction to WebAssembly for JavaScript DevelopersGetting Started with the Map and Set Typed CollectionsJavaScript Currying: A Practical ExampleHow to Use the Call, Apply, and Bind Functions in JavaScriptHow JavaScript's Proxy Object WorksJavaScript waitFor PollingHow to measure page loading time with Performance API Processing images with sharp in Node.js by Pascal Akunne https://blog.logrocket.com/processing-images-sharp-node-js/ Whenever images are to be processed in a NodeJS application, one is well advised to use the Sharp library. Pascal introduces the most important functions in his article. Use console.log() like a pro! by Marko Denic https://denic.hashnode.dev/use-consolelog-like-a-pro Using console.log() for JavaScript debugging is the most common practice among developers. But, there is a lot more, as Marko shows us. Simple Swipe with Vanilla JavaScript by Ana Tudor https://css-tricks.com/simple-swipe-with-vanilla-javascript/ This 2018 article by Ana walks you step-by-step through implementing swipe gestures using an image gallery example with the least amount of code she could think of. The File System Access API: simplifying access to local files by Thomas Steiner & Pete LePage https://web.dev/file-system-access/ The File System Access API allows web apps to read or save changes directly to files and folders on the user's device. Thomas and Peter, both working at Googles Chrome team, show us with examples how this works. An introduction to WebAssembly for JavaScript Developers by Pascal Pares https://pascalpares.appspot.ovh/webassembly-for-javascript-developers/ Pascals article from 2021 is based on the specs of WebAssembly from 2019 and is very useful as an introduction for JavaScript Developers, because it includes a lot of sample code. Getting Started with the Map and Set Typed Collections by Robert Laws https://javascript.plainenglish.io/javascript-getting-started-with-the-map-and-set-typed-collections-2ba173b0ce9f JavaScript has not only the classic arrays to offer, but also other objects for collecting data. Robert has a brief description of Set and Map. JavaScript Currying: A Practical Example by Karthick Ragavendran https://javascript.plainenglish.io/javascript-currying-practical-example-512cf1099e81 In mathematics and computer science, currying is the technique of converting a function that takes multiple arguments into a sequence of functions, each of which takes a single argument … a better description than Karthick’s introduction I could not think of either How to Use the Call, Apply, and Bind Functions in JavaScript by Keyur Paralkar https://www.freecodecamp.org/news/understand-call-apply-and-bind-in-javascript-with-examples/ Call, Apply and Bind are not so common in daily JavaScript development, but they are useful tools because they can be used to change the execution context, as Keyur shows us. How JavaScript's Proxy Object Works by Keyur Paralkar https://www.freecodecamp.org/news/javascript-proxy-object/ Again Keyur. In this post he goes into detail about the proxy object, helps you create another object on behalf of the original object, to get more control over the interaction with the original object JavaScript waitFor Polling by David Walsh https://davidwalsh.name/waitfor By now, every JavaScript developer should be familiar with asynchronous programming using async/await and Promises. But there are use cases where polling makes more sense, as David shows. How to measure page loading time with Performance API by Silvestar Bistrović https://www.silvestar.codes/articles/how-to-measure-page-loading-time-with-performance-api/ MDN recommends using the Performance API to gauge the performance of websites and web applications. It’s useful to show how much time did it take to load a page.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Versengold in Concert","subtitle":"Photos from the performance in Speyer as part of the MPS","date":"2023-09-10","updated":"2023-09-10","path":"post/Versengold-in-Concert/","permalink":"https://kiko.io/post/Versengold-in-Concert/","excerpt":"Since my youth with I hardrock/metal fan, but from good music I let myself convince, even if it does not fit into this scheme. This is what happened with the German medieval/folk band Versengold from Bremen, to whose concert in Bochum my better half dragged me one day. And what can I say … the guys are so much fun with their easy-going manner, their good, funny and sometimes profound German lyrics and their shanty-like music, from which the North German sailor tradition can be clearly heard.","keywords":"youth hardrock/metal fan good music convince fit scheme happened german medieval˿olk band versengold bremen concert bochum half dragged day … guys fun easy-going manner funny profound lyrics shanty-like north sailor tradition heard","text":"Since my youth with I hardrock/metal fan, but from good music I let myself convince, even if it does not fit into this scheme. This is what happened with the German medieval/folk band Versengold from Bremen, to whose concert in Bochum my better half dragged me one day. And what can I say … the guys are so much fun with their easy-going manner, their good, funny and sometimes profound German lyrics and their shanty-like music, from which the North German sailor tradition can be clearly heard. In the meantime we were on two more concerts in Frankfurt, then in Marburg (Open-Air) and in Mainz on the “Night of the Ballads”. Most recently we saw them two weeks ago at the MPS (“Mittelalterlich Phantasie Spectaculum”, a medieval festival) in Speyer, where they played open-air with other bands from the “industry”. On the one hand, I think it’s great that the guys, even if they now fill large halls in Germany, have not forgotten their origins, and on the other hand, I now had the opportunity to get very close to the stage with my camera. Here are the results: let macy_j3et5x = new Macy({ container: '#image-masonry-j3et5x', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); Love you guys…!","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Concert","slug":"Concert","permalink":"https://kiko.io/tags/Concert/"}]},{"title":"SVWW vs. Schalke @ 2023-09-02","subtitle":"The fight of the promoted against the relegated","series":"SV Wehen Wiesbaden","date":"2023-09-02","updated":"2023-09-02","path":"post/SVWW-vs-Schalke-2023-09-02/","permalink":"https://kiko.io/post/SVWW-vs-Schalke-2023-09-02/","excerpt":"1:1 On this game, some in my circle of friends have feverishly awaited, especially my neighbor and friend, who for years is an ardent fan of one of the traditional clubs Schalke 04. We got six additional tickets for the game in time and with a crowd of 11,003 fans, this was also urgently necessary. The stadium (12,566 standing and seated) was full to the roof. Only one of three blocks of the guests was empty. Some Schalke fans seem to have expected nothing from the game in Wiesbaden. No wonder after table position 15 after the last match day. Since season ticket holders get into the stadium a little faster and my friends sat a little scattered in different blocks, I lost sight of them at some point, but that was not tragic, because they had fun. To my delight, the booth operators, who I had to criticize last time, actually did a better job today. Two cash registers: one for cash and another for card/smartphone/watch payers. The sale of beer and bratwurst went much more quickly, only I had to stand in line a bit at the fan shop, because I was not yet recognizable as a fan: it had to get a cap and a jersey, of course, finally.","keywords":"game circle friends feverishly awaited neighbor friend years ardent fan traditional clubs schalke additional tickets time crowd fans urgently stadium standing seated full roof blocks guests empty expected wiesbaden table position match day season ticket holders faster sat scattered lost sight point tragic fun delight booth operators criticize job today cash registers card/smartphone/watch payers sale beer bratwurst quickly stand line bit shop recognizable cap jersey finally","text":"1:1 On this game, some in my circle of friends have feverishly awaited, especially my neighbor and friend, who for years is an ardent fan of one of the traditional clubs Schalke 04. We got six additional tickets for the game in time and with a crowd of 11,003 fans, this was also urgently necessary. The stadium (12,566 standing and seated) was full to the roof. Only one of three blocks of the guests was empty. Some Schalke fans seem to have expected nothing from the game in Wiesbaden. No wonder after table position 15 after the last match day. Since season ticket holders get into the stadium a little faster and my friends sat a little scattered in different blocks, I lost sight of them at some point, but that was not tragic, because they had fun. To my delight, the booth operators, who I had to criticize last time, actually did a better job today. Two cash registers: one for cash and another for card/smartphone/watch payers. The sale of beer and bratwurst went much more quickly, only I had to stand in line a bit at the fan shop, because I was not yet recognizable as a fan: it had to get a cap and a jersey, of course, finally. My fellow fans in row 1 were rather quiet today, despite the good weather and the atmosphere around. In my back there was now and then a comment like “Now RUN!” or “Go ahead. The goal is in THIS direction!”, but I could understand them, because our team hardly dared to cross the center line in the first half. I still don’t know the name of the nice lady next to me, but I will. She suffered today like many rather quietly. The GameIn the first half we were, as they say in the jargon “compact in defense”, which means nothing else, that no one dared to run forward with the ball. Except for once, when after about 20 minutes Kianz Froese, our Canadian striker, ran into the box on the left side at full gallop, but unfortunately missed the goal by a hair’s breadth. Damn! Our team let the opponent play and again straddled every ball off their feet. It wasn’t, however, that Schalke dominated the game. We carelessly let them just do it and the fans had their fun when they shot the ball into the clouds again after a lot of roaring and stomping. Funny. At the beginning of the second half, this continued seamlessly, even if you noticed that the coach in the cabin must have yelled at one or the other and these were now somewhat more active … and suddenly the ball was in our net … 0:1! What? How? A goal in the 54. minute for Schalke out of (almost) nothing! I have to watch that again on TV … :| let macy_694dqk = new Macy({ container: '#image-masonry-694dqk', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); After our guys had digested the shock, after 10 minutes or so, they began … yes indeed … to play football! Also because Kauczinski had replaced a few snoozers. In the last 20 minutes it went with steam on the opponents goal, but we ran slightly out of time. In the 90th minute, 6 minutes of injury time were displayed and it was almost hectic … kick and rush. With success! In the 95th minute it was 1:1, thanks to the leg of Reinthaler, from which bounced a defensive attempt, and in the 97th almost 2:1 … huuuuh, damn close. Before the game I had bet on a 1:1 and was unfortunately/fortunately right. It felt like a victory. ⇾ Match report on kicker.de ConclusionToday we are in 6th place in the table and I don’t really understand why the team was so anxious. Yes, it’s Schalke 04, but they’re further down so far and we certainly don’t need to hide!","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"},{"name":"2. Bundesliga","slug":"2-Bundesliga","permalink":"https://kiko.io/tags/2-Bundesliga/"}]},{"title":"Image Masonry Tag Plugin for Hexo","subtitle":"Easy use of the wonderful Macy.js library to display images in posts","date":"2023-09-01","updated":"2023-09-01","path":"post/Image-Masonry-Tag-Plugin-for-Hexo/","permalink":"https://kiko.io/post/Image-Masonry-Tag-Plugin-for-Hexo/","excerpt":"Displaying a few more images than usual in a post is always a bit tricky, because you have to make sure they don’t get too big and drown out the text. But they should not be too small either and the arrangement is also important to consider. For this purpose I have so far used my Image Slider Tag Plugin, but with this you only ever see one of the images and have to scroll through the rest horizontally. A medium sized overview, best in the so called masonry format, where images are automatically assembled based on their size on a limited area, would be much better for some cases. There are a variety of CSS or JavaScript solutions out there on the net, but the most suitable for me was Macy.js … and how I integrated it into my Hexo environment is what I want to describe here.","keywords":"displaying images usual post bit tricky make dont big drown text small arrangement important purpose image slider tag plugin scroll rest horizontally medium sized overview called masonry format automatically assembled based size limited area cases variety css javascript solutions net suitable macyjs … integrated hexo environment describe","text":"Displaying a few more images than usual in a post is always a bit tricky, because you have to make sure they don’t get too big and drown out the text. But they should not be too small either and the arrangement is also important to consider. For this purpose I have so far used my Image Slider Tag Plugin, but with this you only ever see one of the images and have to scroll through the rest horizontally. A medium sized overview, best in the so called masonry format, where images are automatically assembled based on their size on a limited area, would be much better for some cases. There are a variety of CSS or JavaScript solutions out there on the net, but the most suitable for me was Macy.js … and how I integrated it into my Hexo environment is what I want to describe here. Like (Tiny Slider), Macy.js is also based on JavaScript, as the name already expresses. The setup in HTML is very simple: a certain number of wrappers are arranged in a container, each of which contains an image: 1234567891011121314151617181920212223<div id="macy-container"> <div> <img src="/photos/normal/D50_0053.jpg" alt=""> </div> <div> <img src="/photos/normal/D50_0075.jpg" alt=""> </div> <div> <img src="/photos/normal/D50_0086.jpg" alt=""> </div> <div> <img src="/photos/normal/D50_1092.jpg" alt=""> </div> <div> <img src="/photos/normal/D50_1147.jpg" alt=""> </div> <div> <img src="/photos/normal/D50_1577.jpg" alt=""> </div> <div> <img src="/photos/normal/_D50_3251.jpg" alt=""> </div></div> It does not matter whether the images are the same size or whether they are in portrait or landscape format. Macy.js then takes care of the sensible arrangement of the images in the container. All that is missing now is the call to the script: 1234567891011121314151617181920212223let macy = new Macy({ container: '#macy-container', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 1024: { margin: { x: 8, y: 8 }, columns: 4 }, 768: 3 }}); For more information on the available parameters (and there are some interesting), please visit https://github.com/bigbite/macy.js. The Tag PluginTo make it easy for Hexo users, I created a tag plugin from the code above and added it to my Hexo Tag Plugin Collection on GitHub. Usage Example: 12345678910{% image_masonry "../../photos/normal/D50_0053.jpg|Thomas' Ruby Prince I" "../../photos/normal/_D50_3251.jpg|No Name" "../../photos/normal/D50_0086.jpg|Thomas' German Flag" "../../photos/normal/D50_1147.jpg|Poppy Green" "../../photos/normal/D50_0075.jpg|Thomas Wild Tulips" "../../photos/normal/D50_7474.jpg|Garden Beauties XIV" "../../photos/normal/D50_4451.jpg|Garden Beauties I" "../../photos/normal/D50_1577.jpg|Floral Magic XIV"%} Live Output: let macy_k0yq6b = new Macy({ container: '#image-masonry-k0yq6b', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } });","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"}]},{"title":"Convert HTML into Plain Text in Hexo","subtitle":"Render Page.Excerpt into META Tag","series":"A New Blog","date":"2023-08-31","updated":"2023-08-31","path":"post/Convert-HTML-into-Plain-Text-in-Hexo/","permalink":"https://kiko.io/post/Convert-HTML-into-Plain-Text-in-Hexo/","excerpt":"Hexo, on which this blog is based, is a Static Site Generator (SSG) that generates a whole structure of HTML files from individual Markdown files in which the articles were written. Besides the actual posts, also overview pages like the archives and others. For the latter, however, it only needs an excerpt from the actual article, which Hexo automatically creates from the initially generated HTML content and which is also available as HTML. For my Page Meta dialog, however, I recently needed the excerpt as plain text to make it easier to transfer it manually to a Mastodon post, for example. My initial attempts to extract the plain text from the original Markdown turned out to be quite difficult, because in Hexo not only Markdown is used, but also special Tag Plugins in Nunjucks format and of course plain HTML. Long speech, short sense … after the first dozen RegEx-Replace calls, I got doubts to be on the right way and remembered Page.Excerpt, the variant already generated by Hexo in HTML.","keywords":"hexo blog based static site generator ssg generates structure html files individual markdown articles written actual posts overview pages archives excerpt article automatically creates initially generated content page meta dialog recently needed plain text make easier transfer manually mastodon post initial attempts extract original turned difficult special tag plugins nunjucks format long speech short sense … dozen regex-replace calls doubts remembered pageexcerpt variant","text":"Hexo, on which this blog is based, is a Static Site Generator (SSG) that generates a whole structure of HTML files from individual Markdown files in which the articles were written. Besides the actual posts, also overview pages like the archives and others. For the latter, however, it only needs an excerpt from the actual article, which Hexo automatically creates from the initially generated HTML content and which is also available as HTML. For my Page Meta dialog, however, I recently needed the excerpt as plain text to make it easier to transfer it manually to a Mastodon post, for example. My initial attempts to extract the plain text from the original Markdown turned out to be quite difficult, because in Hexo not only Markdown is used, but also special Tag Plugins in Nunjucks format and of course plain HTML. Long speech, short sense … after the first dozen RegEx-Replace calls, I got doubts to be on the right way and remembered Page.Excerpt, the variant already generated by Hexo in HTML. Now you would think that JavaScript has some built-in function to extract the plain text out of a bunch of HTML tags, but this is actually not the case. You have to take a little detour to do this: 1234567function convertHtml2PlainText(excerpt) { let e = document.createElement("div"); e.innerHTML = excerpt; return e.textContent || e.innerText;}let plainText = convertHtml2PlainText(page.excerpt); Fine, my problem is solved … hmm… NO, because Node.js does not know a document, because a DOM exists only in the browser. But … there are libraries like jsdom that make a DOM available in Node.js: 12345678910const { JSDOM } = require("jsdom");function convertHtml2PlainText(excerpt) { const dom = new JSDOM('<!DOCTYPE html>'); let e = dom.window.document.createElement("div"); e.innerHTML = excerpt; return e.textContent || e.innerText;}let plainText = convertHtml2PlainText(page.excerpt); Nice … but also doesn’t work, because I need the piece of code in an EJS template, but when processing the same to HTML, the included JavaScript code is executed, but loading external libraries via require() is not supported. And once again Hexo’s Tag Helpers come to my rescue: helper-excerpt-plain.js12345678910const { JSDOM } = require("jsdom");hexo.extend.helper.register('excerpt_plain', function(excerpt){ const dom = new JSDOM('<!DOCTYPE html>'); let e = dom.window.document.createElement("div"); e.innerHTML = excerpt; return e.textContent || e.innerText;}); For the sake of beauty, I also cut out leading and double line breaks after the conversion and put the result in a custom meta tag in the head of the HTML page to have access to it later via JavaScript running in the browser: head.ejs123456789101112...let excerpt = excerpt_plain(page.excerpt) .replace(/^(\\r\\n|\\n|\\r)/, "") // Remove leading break .replace(/(\\r\\n|\\n|\\r){2,}/g, " ") // Remove multiple breaks .trim();...<meta name="excerpt" content="<%= excerpt %>">... Et voilá … I have my excerpt as plain text to show in my Page Meta dialog.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Node.js","slug":"Node-js","permalink":"https://kiko.io/tags/Node-js/"},{"name":"Meta","slug":"Meta","permalink":"https://kiko.io/tags/Meta/"}]},{"title":"Pool Photo Generator","subtitle":"How to create multiple device dependent header photos with Node","date":"2023-08-20","updated":"2023-08-20","path":"post/Pool-Photo-Generator/","permalink":"https://kiko.io/post/Pool-Photo-Generator/","excerpt":"Since the existence of this blog, the posts all have a custom header image that I generate from my own photos. Already three years ago (omg … really?) I described in an article how to do this with Hexo: Automatic Header Images in Hexo. To keep it short, I use a pool folder for this, in which I keep in subfolders next to a meta.txt, for the title of the image and an external url on 500px for interactions to the image, three variants that I need for a new post: mobile.jpg (width 480px) tablet.jpg (width 768px) normal.jpg (width 1280px) The only piece of the puzzle that was still missing was the automatic generation of these three image variants and the meta file based on a selected photo that I want to add to the pool of available header images. So far it was fun to generate the header images manually either on the desktop or on the smartphone, but it really doesn’t have to be. My goal now was to write a script where I just throw a selected photo into a folder and the NodeJS script does the rest. My photo workflow is based on Adobe Lightroom Classic and one of the steps is to give a title to the good ones I use here as well. So the script had to include four steps when iterating over the inbound folder’s JPG files: create new pool folder read meta data (IPTC -> title) and write it to meta.txt create the three image variants delete the processed image from the inbound folder","keywords":"existence blog posts custom header image generate photos years ago omg … article hexo automatic images short pool folder subfolders metatxt title external url 500px interactions variants post mobilejpg width 480px tabletjpg 768px normaljpg 1280px piece puzzle missing generation meta file based selected photo add fun manually desktop smartphone doesnt goal write script throw nodejs rest workflow adobe lightroom classic steps give good include iterating inbound folders jpg files create read data iptc -> delete processed","text":"Since the existence of this blog, the posts all have a custom header image that I generate from my own photos. Already three years ago (omg … really?) I described in an article how to do this with Hexo: Automatic Header Images in Hexo. To keep it short, I use a pool folder for this, in which I keep in subfolders next to a meta.txt, for the title of the image and an external url on 500px for interactions to the image, three variants that I need for a new post: mobile.jpg (width 480px) tablet.jpg (width 768px) normal.jpg (width 1280px) The only piece of the puzzle that was still missing was the automatic generation of these three image variants and the meta file based on a selected photo that I want to add to the pool of available header images. So far it was fun to generate the header images manually either on the desktop or on the smartphone, but it really doesn’t have to be. My goal now was to write a script where I just throw a selected photo into a folder and the NodeJS script does the rest. My photo workflow is based on Adobe Lightroom Classic and one of the steps is to give a title to the good ones I use here as well. So the script had to include four steps when iterating over the inbound folder’s JPG files: create new pool folder read meta data (IPTC -> title) and write it to meta.txt create the three image variants delete the processed image from the inbound folder The ScriptI implemented the script as a class with the following skeleton: pool-photo-generator.cjs123456789101112131415161718192021222324252627282930'use strict';[requirements ...][vars ...]class PoolPhotoGenerator { /** * Contructor of PoolPhotoGenerator * @param {String} inboundFolder * @param {String} poolFolder */ constructor(inboundFolder, poolFolder) { ... } /** * Runs the generation of inbound photos to pool photos */ generate() { ... } /** * Helper function to create image variant * @param {String} imgSource * @param {String} imgTarget * @param {Number} sizeWidth */ async createImageVariant(imgSource, imgTarget, sizeWidth) { ... }}module.exports.PoolPhotoGenerator = PoolPhotoGenerator; RequirementsTo handle files and folders in NodeJS you need at least fs and path: 12const fs = require("fs");const path = require("path"); For image processing there’s no better solution as Sharp: 12const sharp = require('sharp');sharp.cache(false); //prevents keeping source file open Similarly powerful, but intended for reading image metadata is EXIFR: 1const exifr = require('exifr'); VarsI just needed three vars for holding the full qualified path of the current execution folder and the names of the two incoming parameters: 1234const _currentPath = __dirname;let _inboundFolder;let _poolFolder; ConstructorIn this case, the constructor only serves to provide and check the necessary parameters of the class: 123456789101112constructor(inboundFolder, poolFolder) { _inboundFolder = path.join(_currentPath, inboundFolder); _poolFolder = path.join(_currentPath, poolFolder); if (!fs.existsSync(_inboundFolder)) { throw "Inbound folder not found" } if (!fs.existsSync(_poolFolder)) { throw "Pool folder not found" }} Function ‘generate’This is the main function to call, and it first reads the input folder for JPG and cycles through all the hits. Then for each file the above four steps are executed: 12345678910111213141516171819202122232425262728293031323334353637383940generate() { let self = this; const inboundFiles = fs.readdirSync(_inboundFolder); const jpgFiles = inboundFiles.filter(file => { return path.extname(file).toLowerCase() === ".jpg"; }); jpgFiles.forEach((file) => { const imgFile = path.join(_inboundFolder, file); // Step 1: Create new pool folder const newPhotoFolder = path.join(_poolFolder, file.replace(path.extname(file), '')); fs.mkdirSync(newPhotoFolder); // Step 2: Read TITLE from IPTC and write to meta.txt const iptcMeta = exifr.parse(imgFile, { iptc: true }).then(output => { let title = output.ObjectName || "No Title"; fs.writeFile(path.join(newPhotoFolder, "meta.txt"), title); }); // Step 3: Create image variants const createMobile = self.createImageVariant(imgFile, path.join(newPhotoFolder, "mobile.jpg"), 480); const createTablet = self.createImageVariant(imgFile, path.join(newPhotoFolder, "tablet.jpg"), 768); const createNormal = self.createImageVariant(imgFile, path.join(newPhotoFolder, "normal.jpg"), 1280); // Step 4: Delete processed JPG in inbound folder, when everything is done Promise.all([ iptcMeta, createMobile, createTablet, createNormal ]).then(() => { fs.unlinkSync(imgFile); }); }} Function ‘createImageVariant’This helper function reduces the original image to the desired size and saves it in the destination (pool) folder as a JPG: 12345678910async createImageVariant(imgSource, imgTarget, sizeWidth) { await sharp(imgSource) .resize({ fit: sharp.fit.contain, width: sizeWidth }) .jpeg({ quality: 90, mozjpeg: true }) .toFile(imgTarget);} In the above code I have omitted some syntactical sugar. You can find the complete script here: https://github.com/kristofzerbe/kiko.io/blob/master/lib/pool-photo-generator.cjs The RunnerI integrated the call to the generator into my Hexo workflow, but also wrote a small runner to run it independently: 1234567891011121314/** * This is only for executing the selector manually. * * Execution: * node "./lib/_run_pool-photo-generator.cjs" */const PoolPhotoGenerator = require("../lib/pool-photo-generator.cjs").PoolPhotoGenerator;const inboundFolder = "../new_photos"; //my inbound folderconst poolFolder = "../static/pool"; //my pool folderconst generator = new PoolPhotoGenerator(inboundFolder, poolFolder);generator.generate(); ConclusionLast but not least, in my existing Lightroom workflow, I configured the wonderful plugin Jeffrey’s “Collection Publisher” to create the new pool photos directly through it into the Inbound folder. Once the changes are committed to Github, where the blog is hosted and the deployment action happens, the new header images are created and displayed at https:\\kiko.io\\photos and are available for a new post.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"},{"name":"Node.js","slug":"Node-js","permalink":"https://kiko.io/tags/Node-js/"}]},{"title":"SVWW vs. Karlsruhe @ 2023-08-18","subtitle":"My first home game in this season","series":"SV Wehen Wiesbaden","date":"2023-08-19","updated":"2023-08-19","path":"post/SVWW-vs-Karlsruhe-2023-08-18/","permalink":"https://kiko.io/post/SVWW-vs-Karlsruhe-2023-08-18/","excerpt":"1:0 I already had my season ticket on the first matchday of the 2023/2024 season, but was unable to get into the stadium for the team’s first home game and thus missed the 1:1 draw against Magdeburg. First ImpressionsSo, Friday was my premiere on my season seat West 3, Row 1, Seat 1. That this seat was free when I booked it was a coincidence, but being so close to the pitch appealed to me. In front of me, the concrete railing of the spectator block, where I can put my beer, and directly below it, the SVWW coach’s bench under a plexiglass roof. You can hardly get any closer to the team.","keywords":"season ticket matchday 2023 unable stadium teams home game missed draw magdeburg impressionsso friday premiere seat west row free booked coincidence close pitch appealed front concrete railing spectator block put beer directly svww coachs bench plexiglass roof closer team","text":"1:0 I already had my season ticket on the first matchday of the 2023/2024 season, but was unable to get into the stadium for the team’s first home game and thus missed the 1:1 draw against Magdeburg. First ImpressionsSo, Friday was my premiere on my season seat West 3, Row 1, Seat 1. That this seat was free when I booked it was a coincidence, but being so close to the pitch appealed to me. In front of me, the concrete railing of the spectator block, where I can put my beer, and directly below it, the SVWW coach’s bench under a plexiglass roof. You can hardly get any closer to the team. However, the division of the row of seats is quite stupid, with the separation of Block West 2 and West 3 being quite arbitrary. With the result that I have to push my way to seat 1 across almost 20 seats if I am late. From the entrance of block West 2, on the other hand, it would only be just under 10 seats, but the stewards won’t let me get to my seat via West 2 because of my card and for the sake of German order :| My seat neighbour on the right side is a man of the “Do I have to talk?” type who can hardly get his teeth apart even during the game. On my left, however, sits a nice older lady with short grey hair who must already be part of the inventory. A permanent season ticket holder, I guess, who doesn’t yell all the time either, but seems to be a true fan. The first question directed at me was “But you weren’t there for the first game!?”, which immediately triggered a kind of guilt in me, “Yes, I didn’t cheer for them in the first game. I’m sooo sorry”. We’ll get on well, especially as I took her deposit cup at half-time and cashed it in. We’re already friends, even if I don’t know her name yet. Speaking of beverage supply: The club has a lot of stands around the stadium, but they are so badly organised that it’s hard to make it back to your seat in time. There were four staff in the snack van right next to Block West 3, one for ordering and collecting money and the other three for serving. The cashier was so slow that the other three had nothing to do the whole time and the queue in front of the stand got longer and longer and some people just gave up. Dear operator: This would happen much faster with two cashiers. It may be that they are all volunteers, but that can be organised much better for the benefit of the fans! In the row directly behind me sits a group of older men who spout rather platitudinous football wisdom all the time. These guys would be a perfect model for the German (but unfortunately already deceased) comedian Loriot :DSitting next to them, however, are two or three guys in their 50s who don’t really have their emotions under control. One of them, in particular, yells “Referee, hang yourself” or something similarly stupid and insults opposing players when they go down in a tackle. I, on the other hand, only ever shouted positively at my own boys. “Let’s go”, “Go on, go on, go on”. And of course I sing along, because no one here notices how “well” I can do that ;) The GameThe club has of course strengthened itself for the new season with new players in all sections of the team, whom I have now seen play for the first time. And unfortunately, some of them, such as Brooklyn Ezeh and Benedikt Hollerbach, who grew on me during the few games last year, have moved to other clubs. let macy_2c0ap0 = new Macy({ container: '#image-masonry-2c0ap0', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); But we quickly saw that our coach, Markus Kauczinski, knows his ex-club Karlsruher FC well. At the beginning of the first half, the players of this traditional club directly tried to overrun our team, seen by 10,626 spectators. There was a lot of pressure on the defence, but it held up magnificently. There was always a Wiesbaden leg between the ball and the goal and we limited ourselves to counterattacks, which led to success in the 22nd minute when the newcomer from Bayern Munich Lee Hyun-Ju marked the 1:0. The rest of the first half was quite even, because Karlsruhe had lost the momentum a little bit, but they found it again at the beginning of the second half. Again, the game ran in only one direction, our goal, but the last 16 metres, in the box, were impregnable for them and when they did manage it, they failed because of our goalkeeper Florian Stritzel. ⇾ Match report on kicker.de ConclusionSV Wehen Wiesbaden is certainly the underdog in the 2. Bundesliga and no fan expects that at the end of the season it will be about more than keeping the league. I like underdogs and am therefore completely thrilled about this victory, especially since the table looked really good at the end of the day: In two weeks, an even bigger German football heavyweight will come to Wiesbaden: Schalke 04. Let’s see if the boys have grown a few more legs by then. We will need them …","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"},{"name":"2. Bundesliga","slug":"2-Bundesliga","permalink":"https://kiko.io/tags/2-Bundesliga/"}]},{"title":"My Hometown, My Club","subtitle":"How I became a football fan over 50","series":"SV Wehen Wiesbaden","date":"2023-08-19","updated":"2023-08-19","path":"post/My-Hometown-My-Club/","permalink":"https://kiko.io/post/My-Hometown-My-Club/","excerpt":"I was born in Wiesbaden (Hesse, Germany) and I consider this city my home, even though my father built a house in a small suburb called Taunusstein-Wehen in the 70s and I practically grew up there. In this small town, where practically everybody knows everybody, there is a small football club called SV Wehen since 1926 and some of my schoolmates played there in their youth. This small club played only a regional role at all until 1979, when a local business man put money into the club and it worked its way up one league after the other over the years:","keywords":"born wiesbaden hesse germany city home father built house small suburb called taunusstein-wehen 70s practically grew town football club sv wehen schoolmates played youth regional role local business man put money worked league years","text":"I was born in Wiesbaden (Hesse, Germany) and I consider this city my home, even though my father built a house in a small suburb called Taunusstein-Wehen in the 70s and I practically grew up there. In this small town, where practically everybody knows everybody, there is a small football club called SV Wehen since 1926 and some of my schoolmates played there in their youth. This small club played only a regional role at all until 1979, when a local business man put money into the club and it worked its way up one league after the other over the years: 1983 Bezirksliga (amateurs)⇣1987 Landesliga (amateurs)⇣1989 Oberliga (amateurs)⇣1994 Regionalliga (semi-professional)⇣2007 2. Bundesliga (professional) However, with the club’s promotion to the second highest and now professional league of German football, the requirements for the stadium and the necessary infrastructure also changed and the old one in Wehen did not meet the requirements of the officials. So the professional football department was spun off into a company called SV Wehen Wiesbaden based in the nearby Hessian capital, where the new Brita Arena was built. They were relegated to the newly (in 2008) formed 3. Bundesliga after 2 years, but the new club remained in Wiesbaden and in 2020 it was again promoted to the 2. Bundesliga, but was relegated again one year later. I was always only semi-interested in football, especially since I became a fan of the Hamburger Sportverein (HSV) by chance in the early 80s, but in 2022 I got free tickets to a SV Wehen Wiesbaden game through my wife’s colleague … and it was actually really fun, with a cup of beer and a bratwurst, to cheer on the guys on the pitch who were playing professional football in my hometown. I was a little bit hooked … At the end of the 2022/2023 season, the club finished 3rd in the 3. Bundesliga, having missed out on direct promotion, but qualifying for relegation against the third last-place team in the next higher league: Arminia Bielefeld. The two incredible relegation matches were actually won by the underrated club, 4:0 and 2:1 respectively. YESSSS! Somehow it seemed clear to me to go to the stadium more often from now on and the fact that this year the 2nd league is peppered with German traditional clubs (like HSV, my childhood club) made me buy a season ticket for the first time: Block West 3, Row 1, Seat 1 … \\o/.","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"}]},{"title":"CONTINUE READING Link & Auto Scrolling on the called page","subtitle":"Help your user to read on directly","date":"2023-07-29","updated":"2023-07-29","path":"post/CONTINUE-READING-Link-Auto-Scrolling-on-the-called-page/","permalink":"https://kiko.io/post/CONTINUE-READING-Link-Auto-Scrolling-on-the-called-page/","excerpt":"On the home page of a blog or other text-heavy site with regular new articles, it is often advisable not to display the entire text of the article, but a more or less large excerpt and a READ MORE or CONTINUE READING link that leads to the rest of the article, usually a stand-alone article page. This allows the user to quickly get a picture of, say, the last dozen posts when he comes here to browse your texts. However, it is somewhat unpleasant if you as the author decide to display a larger excerpt after all, and the user lands at the top of the called page after clicking on the MORE link and first has to scroll/navigate to the right place until he can resume reading. This destroys his reading flow. It is better to take the user directly to the page where the MORE link interrupted the text on the home page. With a hash and some JavaScript this is done so quickly, that I wonder why I haven’t implemented this on my own blog already :)","keywords":"home page blog text-heavy site regular articles advisable display entire text article large excerpt read continue reading link leads rest stand-alone user quickly picture dozen posts browse texts unpleasant author decide larger lands top called clicking scroll/navigate place resume destroys flow directly interrupted hash javascript havent implemented","text":"On the home page of a blog or other text-heavy site with regular new articles, it is often advisable not to display the entire text of the article, but a more or less large excerpt and a READ MORE or CONTINUE READING link that leads to the rest of the article, usually a stand-alone article page. This allows the user to quickly get a picture of, say, the last dozen posts when he comes here to browse your texts. However, it is somewhat unpleasant if you as the author decide to display a larger excerpt after all, and the user lands at the top of the called page after clicking on the MORE link and first has to scroll/navigate to the right place until he can resume reading. This destroys his reading flow. It is better to take the user directly to the page where the MORE link interrupted the text on the home page. With a hash and some JavaScript this is done so quickly, that I wonder why I haven’t implemented this on my own blog already :) The principle is quite simple: append a hash to the MORE link URL the user will click detect the hash on the called article page and scroll via JavaScript to an anchor element that was rendered here instead of the MORE link remove the hash from the URL again Some pre-explanation of my exampleMy blog is based on the static website generator Hexo. So I write my articles in Markdown and use for the interruption of an article a built-in helper, which uses the comment <!-- more --> to hack the Content into two parts (Excerpt and More) and replaces it with simple <span id="more"></span>. I use the Excerpt for the start and other overview pages and the Content for the article pages themselves. Expand the MORE linkIn my template for the start page, 8 articles are currently rendered into the page via a separate EXCERPT template. This contains the code for the MORE link (excerpt_link in my example): ../layout/partial/excerpt.ejs12345678910111213141516...<div class="article-entry"> <%- post.excerpt %> <% if (theme.excerpt_link){ %> <p class="article-more-link"> <a href="<%- url_for(post.path) + '#continue' %>"> <%= theme.excerpt_link %> </a> </p> <% } %></div>... I simply appended the hash “#continue“ to the url_for(post.path) statement and this is it for the start page. The JS on the article pageSince some of my pages deal with hashes, I have a general script that loads in the footer of each page and checks the hashes passed to the page. In the switch statement I only needed a new case for continue. The first thing I did was to remove the hash again from the because I don’t want to show that to the user. It is only a tool for a better reading flow and should not have any further effects. To scroll around on a page via JavaScript, there is the scrollIntoView method, which provides a wonderfully smooth scrolling effect with the behavior: "smooth" and inline: "nearest" options. In my case, however, this was out of the question, since it does not support an offset, which I need for my fixed header. The solution was the classic window.scrollTo() function, which is a bit more work, but you can specify exactly where you want the page to go. ../layout/partial/after-footer.ejs123456789101112131415161718192021<script>var hash = window.location.hash.substr(1);switch (hash.toLowerCase()) { ... case "continue": history.replaceState(null, null, ' '); // remove hash window.scrollTo({ // scroll to the MORE element in the Article behavior: 'smooth', top: document.querySelector("#more").getBoundingClientRect().top - document.body.getBoundingClientRect().top - 200, // with a little buffer around it }); break; default: break;}</script> This is it …","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"UI","slug":"UI","permalink":"https://kiko.io/tags/UI/"},{"name":"Usability","slug":"Usability","permalink":"https://kiko.io/tags/Usability/"}]},{"title":"Extension of downupPopup: Back Button, Escape Key & More","subtitle":"Contributing to Ali Dinçer's Bottom Sheet project","date":"2023-07-04","updated":"2023-07-04","path":"post/Extension-of-downupPopup-Back-Button-Escape-Key-More/","permalink":"https://kiko.io/post/Extension-of-downupPopup-Back-Button-Escape-Key-More/","excerpt":"I recently introduced a Bottom Sheet dialog on this blog to display a page’s metadata (like this), using Ali Dincer’s work: downupPopup. I described the way to do this in a post a couple three weeks ago. Shortly after that Koos Looijensteijn triggered me with his post How to make digital business cards and share them via QR codes and I felt like using my newly introduced dialog manager based on downupPopup for my own contact card.But more about that at a later time, respectively blog post… Important for this post is that Ali’s bottom sheet solution did not offer everything I wanted for my implementation: 1. Make Dialog ReusableAs I have already described in the above mentioned article: Ali’s approach to calling the dialog was to create and initialize the necessary HTML elements if it didn’t already exist in the DOM. I pulled out the initialization to make the component reusable. There is now a preparation part and an initialization part and the latter is always called, no matter if another bottom sheet dialog was already created before. My GitHub commit on this part…","keywords":"recently introduced bottom sheet dialog blog display pages metadata ali dincers work downuppopup post couple weeks ago shortly koos looijensteijn triggered make digital business cards share qr codes felt newly manager based contact cardbut time post… important alis solution offer wanted implementation reusableas mentioned article approach calling create initialize html elements didnt exist dom pulled initialization component reusable preparation part called matter created github commit part…","text":"I recently introduced a Bottom Sheet dialog on this blog to display a page’s metadata (like this), using Ali Dincer’s work: downupPopup. I described the way to do this in a post a couple three weeks ago. Shortly after that Koos Looijensteijn triggered me with his post How to make digital business cards and share them via QR codes and I felt like using my newly introduced dialog manager based on downupPopup for my own contact card.But more about that at a later time, respectively blog post… Important for this post is that Ali’s bottom sheet solution did not offer everything I wanted for my implementation: 1. Make Dialog ReusableAs I have already described in the above mentioned article: Ali’s approach to calling the dialog was to create and initialize the necessary HTML elements if it didn’t already exist in the DOM. I pulled out the initialization to make the component reusable. There is now a preparation part and an initialization part and the latter is always called, no matter if another bottom sheet dialog was already created before. My GitHub commit on this part… 2. Dynamic DistanceSince it makes no sense to have to scroll around on a contact card if the space provided by the web designer is not sufficient at small resolutions, I need a more dynamic approach to Ali’s distance, which sets the distance of the bottom sheet to the top of the viewport. To do this, I introduced a new setting called minContentHeight that can specify in pixels how high the content must be at least for everything to be visible. Because this makes the possibly specified distance absurd, I overwrite this specification with a value calculated with it and only then enter the distance value into the element attribute for further use. For the calculation to be correct, however, I need a fixed value for the HEADER of the dialog at this point, which was previously only defined in the CSS. In retrospect, this was a good decision, because once slight pixel shifts between HEADER and CONTENT disappeared in Google’s Chrome, my preferred browser. downupPopup.js12345678910111213141516171819202122232425262728293031// Option Handling...var settings = $.extend({ ... minContentHeight: null, ...});// Initialization...// setting header heightconst $head = $this.find(".downupPopup-header");$head.find("span").text(settings.headerText);const headH = 6;$head.css('height', '' + headH + 'vh');// calculating dynamic distance by given minContentHeightif (settings.minContentHeight) { let calcDistance = Math.round((100 * (window.innerHeight - settings.minContentHeight) / window.innerHeight)) - headH; settings.distance = Math.max(0, calcDistance);}$this.attr('distance', settings.distance);// setting distance to topconst $cont = $this.find(".downupPopup-content");const contH = 100 - settings.distance - headH;$cont.css('height', '' + contH + 'vh');... My GitHub commit on this part… 3. Closing Dialog by ESC Key or BACK ButtonWith Koos’ really nice contact card, I noticed that I automatically tried to close his SHARE DATA popup dialog with the QRCode, which is created on-the-fly with JavaScript, with the BACK button, but ended up on a completely different, previously called page. I guess this is a psychological thing: if an element not bound to the url overlaps the current page, people (or just me?) perceives it as an independent page and try to navigate with long learned methods. So what was missing was, on the one hand, the ability to close the dialog using the ESCAPE key (for desktop users) and, on the other hand, to manipulate the URL history so that the BACK button or the corresponding mobile gesture works as expected. For the latter I needed again a new setting: the hash which will be added to the current URL to make the BACK button work: urlHash. In addition, there were several redundant places in the original code, with which the dialog was closed, which I have summarized in a function, in order to avoid the new necessary to write several times. (my GitHub commit on this part). downupPopup.js1234567891011121314151617181920212223242526272829303132333435363738394041424344// Option Handling...var settings = $.extend({ ... urlHash: null, ...});// General CLOSE functionfunction close() { ... // unbind ESC $(document).off('keyup'); // remove url hash & unbind BACK button if (settings?.urlHash || $this.attr('hash')) { history.replaceState(null, null, ' '); $(window).off("popstate"); }}// Initialization...// bind ESC to close$(document).on('keyup', function(event) { if(event.key == "Escape") { close(); }});if (settings.urlHash) { // set url hash & bind BACK button to close window.location.hash = settings.urlHash; $(window).on("popstate", function(){ close(); });}$this.attr('hash', settings.urlHash);... The key of this code regarding the BACK button is to put the passed hash into the URL when initializing the dialog via window.location.hash and to make sure that it is closed when the hash is removed again by the BACK button. I achieved this by using the popstate event, which fires when the active history entry changes. When the dialog is about to close, I had to make sure that the history entry is overwritten via replaceState with null (delete) and remove the previously bound popstate event again, to avoid side effects. The code for the ESC key is similar: Bind the keyup event on startup to close the dialog and remove it again on close. My GitHub commit on this part… ConclusionEverything works as expected and I hope that Ali will process my pull request soon. Until then you can find the code in my fork. All in all, the script has not become more readable or easier to maintain, even by using jQuery. In my head I already have the idea to do a complete rewrite as ESM class. Reason for this is surely Ali’s approach to realize commands like ‘open’ and ‘close’ as string parameters. This works better than real functions of an instantiated object. Furthermore, it is necessary to cache settings like distance and urlHash in HTML attributes, which does not have to be this way.","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"jQuery","slug":"jQuery","permalink":"https://kiko.io/tags/jQuery/"},{"name":"UI","slug":"UI","permalink":"https://kiko.io/tags/UI/"},{"name":"Contributing","slug":"Contributing","permalink":"https://kiko.io/tags/Contributing/"}]},{"title":"Discoveries #25 - Tutorials & HowTo's","subtitle":null,"series":"Discoveries","date":"2023-07-01","updated":"2023-07-01","path":"post/Discoveries-25-Tutorials-HowTo-s/","permalink":"https://kiko.io/post/Discoveries-25-Tutorials-HowTo-s/","excerpt":"This Discoveries issue is generally about tutorials from different areas: CORS, Azure, Elektron, GitHub Action, SVG. Outstanding articles by outstanding authors, who explain things in an easy and understandable way. Happy Reading… CS Visualized: CORSUse Azure Functions, Azure Storage blobs, and Cosmos DB to copy images from public URLsBuild a Secure Desktop App with Electron Forge and ReactNode.js API Authentication with JSON Web TokensHow To Build an SMTP Mail Server with Express, Node, and GmailAdvanced Git Series 1/8: Creating the Perfect Commit in GitLearn how to use Git and GitHub in a team like a proHow to Build Your First JavaScript GitHub ActionGetting the Gist of GitHub ActionsSwipey Image Grids (with SVG)","keywords":"discoveries issue generally tutorials areas cors azure elektron github action svg outstanding articles authors explain things easy understandable happy reading… cs visualized corsuse functions storage blobs cosmos db copy images public urlsbuild secure desktop app electron forge reactnodejs api authentication json web tokenshow build smtp mail server express node gmailadvanced git series 1/8 creating perfect commit gitlearn team prohow javascript actiongetting gist actionsswipey image grids","text":"This Discoveries issue is generally about tutorials from different areas: CORS, Azure, Elektron, GitHub Action, SVG. Outstanding articles by outstanding authors, who explain things in an easy and understandable way. Happy Reading… CS Visualized: CORSUse Azure Functions, Azure Storage blobs, and Cosmos DB to copy images from public URLsBuild a Secure Desktop App with Electron Forge and ReactNode.js API Authentication with JSON Web TokensHow To Build an SMTP Mail Server with Express, Node, and GmailAdvanced Git Series 1/8: Creating the Perfect Commit in GitLearn how to use Git and GitHub in a team like a proHow to Build Your First JavaScript GitHub ActionGetting the Gist of GitHub ActionsSwipey Image Grids (with SVG) CS Visualized: CORS by Lydia Hallie https://dev.to/lydiahallie/cs-visualized-cors-5b8h What web dev hasn't struggled with CORS errors? Lydia explains here in great detail and with visual support the whys and wherefores of CORS and how to work around problems. Use Azure Functions, Azure Storage blobs, and Cosmos DB to copy images from public URLs by Dave Brock https://daveabrock.com/2020/11/25/images-azure-blobs-cosmos In this post, Dave shows how to work with Azure Storage blobs and Cosmos DB to copy images that are available over the public Internet. Build a Secure Desktop App with Electron Forge and React by Kilian Valkhof https://www.sitepoint.com/electron-forge-react-build-secure-desktop-app/?utm_source=rss Creating a cross-platform desktop app is easy thanks to Electron. Learn from Kilian how to create a secure desktop app using React, Electron and Electron Forge. Node.js API Authentication with JSON Web Tokens by Jay Krishna Reddy https://javascript.plainenglish.io/node-js-api-authentication-with-json-web-tokens-bb511f603723 In this article, Jay shows how to access the JSON web token (JWT) in Node.js and also to protect our routes with it. How To Build an SMTP Mail Server with Express, Node, and Gmail by Michael Rehnert https://daily.dev/blog/how-to-build-an-smtp-mail-server-with-express-node-and-gmail Michael shows in his tutorial how to use Node and Express to develop a mail server that uses Gmail for free transport via SMTP. Advanced Git Series 1/8: Creating the Perfect Commit in Git by Tobias Günther https://css-tricks.com/creating-the-perfect-commit-in-git/ In this series from 2021 on CSS-Tricks, Tobias goes into the most important aspects in the advanced handling of Git in a vivid way. Better commits, branching, collaboration, rebasing, cherry-picking … a good reminder. Learn how to use Git and GitHub in a team like a pro by Damian Demasi https://dev.to/colocodes/learn-how-to-use-git-and-github-in-a-team-like-a-pro-2dk7 This short two-part series by Damian clearly highlights the possibilities of collaboration with Git and the platform Github, which is usually neglected in other tutorials. How to Build Your First JavaScript GitHub Action by Bassem Dghaidi https://www.freecodecamp.org/news/build-your-first-javascript-github-action/ The automation possibilities of Github's Actions seem limitless. There is hardly a JavaScript workflow that cannot be mapped in some way. This is also due to the openness, because anyone can provide workflows. Bassem explains the basic system with examples. Getting the Gist of GitHub Actions by Brendon Smith https://gist.github.com/br3ndonland/f9c753eb27381f97336aa21b8d932be6 Like Bassem, Brendon shows some of the the basic principle of GitHub Actions, but expands on it with pro tips and workarounds when things get stuck. Swipey Image Grids (with SVG) by Cassie Evans https://www.cassie.codes/posts/swipey-image-grids/ If you're familiar with CSS animations, you might be interested in this post by Cassie, as she shows how to do it without CSS directly in SVG.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Include and provide JSON data in Hexo EJS Templates","subtitle":"... with a new Helper and an async function","date":"2023-06-27","updated":"2023-06-27","path":"post/Include-and-provide-JSON-data-in-Hexo-EJS-Templates/","permalink":"https://kiko.io/post/Include-and-provide-JSON-data-in-Hexo-EJS-Templates/","excerpt":"The three main components of a standard installation of the Static Site Generator Hexo are the template system EJS (Embedded JavaScript Templating), Markdown for the content and Stylus for the styles. In the template files are the three main tags for driving content: Scriptlet tag for control flow (no output) 123<% ... my JavaScript code to process data into the template%> Output a value as escaped HTML 1<%= myVariable %> Output of a raw value, usually in the form of a JavaScript function 1<%- myFunction() %> Hexo’s helper system is based on the latter. So you can include a JavaScript file in your template that makes use of the JS Helper in node_modules\\hexo\\lib\\plugins\\helper\\js.js as follows … 1<%- js('/js/dist/myFancyFunctions.js') %> … which will be rendered to: 1<script src="/js/dist//js/dist/qr-code-styling.js"></script> The ProblemSo far and short, so good … but I recently tried to use this way to include a JSON file whose data one of my scripts needed as startup options and I noticed that the above mentioned JS helper unfortunately takes care of the possibly missing file extension js. It doesn’t matter if you only pass the path to the file as a string or if all necessary attributes as an object.","keywords":"main components standard installation static site generator hexo template system ejs embedded javascript templating markdown content stylus styles files tags driving scriptlet tag control flow output 123<% code process data template%> escaped html 1<%= myvariable %> raw form function 1<%- myfunction hexos helper based include file makes js node_modules\\hexo\\lib\\plugins\\helper\\jsjs … js'/js/dist/myfancyfunctionsjs' rendered 1<script src="/js/dist//js/dist/qr-code-stylingjs"></script> problemso short good recently json scripts needed startup options noticed mentioned takes care possibly missing extension doesnt matter pass path string attributes object","text":"The three main components of a standard installation of the Static Site Generator Hexo are the template system EJS (Embedded JavaScript Templating), Markdown for the content and Stylus for the styles. In the template files are the three main tags for driving content: Scriptlet tag for control flow (no output) 123<% ... my JavaScript code to process data into the template%> Output a value as escaped HTML 1<%= myVariable %> Output of a raw value, usually in the form of a JavaScript function 1<%- myFunction() %> Hexo’s helper system is based on the latter. So you can include a JavaScript file in your template that makes use of the JS Helper in node_modules\\hexo\\lib\\plugins\\helper\\js.js as follows … 1<%- js('/js/dist/myFancyFunctions.js') %> … which will be rendered to: 1<script src="/js/dist//js/dist/qr-code-styling.js"></script> The ProblemSo far and short, so good … but I recently tried to use this way to include a JSON file whose data one of my scripts needed as startup options and I noticed that the above mentioned JS helper unfortunately takes care of the possibly missing file extension js. It doesn’t matter if you only pass the path to the file as a string or if all necessary attributes as an object. 1234<%- js({ src: 'js/dist/script-options.json', type: 'application/json'}) %> This code leads to the following wrong code … 12<script src="/js/dist//js/dist/qr-code-styling.json.js" type="application/json"></script> The JSON HelperSince Hexo’s developers went a bit over my/the target with the helper’s functionality, I had to build my own JSON helper, which is actually just a slightly customized copy of the original: themes\\landscape\\scripts\\json-helper.js12345678910111213141516171819202122232425262728const { htmlTag, url_for } = require('hexo-util');hexo.extend.helper.register('json', function(...args){ let result = '\\n'; args.flat(Infinity).forEach(item => { if (typeof item === 'string' || item instanceof String) { // args = String only let path = item; if (!path.endsWith('.json')) { path += '.json'; } result += `<script src="${url_for.call(this, path)}" type="application/json"></script>\\n`; } else { // args = Object -> Custom Attributes item.src = url_for.call(this, item.src); item.type = "application/json"; if (!item.src.endsWith('.json')) item.src += '.json'; result += htmlTag('script', { ...item }, '') + '\\n'; } }); return result;}); You can find the complete file here. With this its possible to reference the JSON like this: 1<%- json('js/dist/myOptions.json') %> Bring JSON data to lifeHowever, the helper only allowed me to load the file as such. What was still missing was the loading of the data in the JavaScript of the page itself. The easiest way to achieve that, was to perform a FETCH of the already referenced and loaded file in the SCRIPT block of the template as an immediately invoked async function: EJS File12345678910111213141516<%- js('js/dist/myFancyObjectLibrary.js') %><%- json({ src: 'js/dist/myOptions.json', id: 'my-options'}) %><script> (async () => { const response = await fetch(document.getElementById('my-options').src); const options = await response.json(); let obj = new myFancyObject(options); //... do something with the initialized object })();</script> Et voilà … Job done.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Templating","slug":"Templating","permalink":"https://kiko.io/tags/Templating/"}]},{"title":"Show pages meta data (JSON-LD) in Bottom Sheet","subtitle":"Visualize the generated meta code on your page in a sliding panel for review and information purposes","date":"2023-06-11","updated":"2023-06-11","path":"post/Show-pages-meta-data-JSON-LD-in-Bottom-Sheet/","permalink":"https://kiko.io/post/Show-pages-meta-data-JSON-LD-in-Bottom-Sheet/","excerpt":"A few months ago I introduced new meta data (JSON-LD) for the pages of this blog and also wrote about my implementation. Works also everything quite well … only the verification of the generated data was a bit cumbersome: Open DevTools for a page in Chrome. Search in the HEAD of the source code for the included script (“application/ld+json”) Copy out JSON-LD code Format JSON into VS code … and check Nothing for now and simply impossible on the smartphone, even if there would be a reasonable Chrome extension for displaying JSON-LD data, but it does not exist (yet). Another problem was that I use automatically generated Socal Media images for my articles, which are included in the JSON-LD, but do not appear anywhere in the page and thus were beyond my control. I simply wanted to display all the generated stuff. Since I’ve been a fan of the so-called bottom sheets since the first version of Google’s Material Design, I imagined a script that grabs the code embedded in the page and pushes a panel with all the data visualized there into the page from the bottom … and the whole act was easier than I thought.","keywords":"months ago introduced meta data json-ld pages blog wrote implementation works … verification generated bit cumbersome open devtools page chrome search head source code included script application/ld+json copy format json check simply impossible smartphone reasonable extension displaying exist problem automatically socal media images articles control wanted display stuff ive fan so-called bottom sheets version googles material design imagined grabs embedded pushes panel visualized act easier thought","text":"A few months ago I introduced new meta data (JSON-LD) for the pages of this blog and also wrote about my implementation. Works also everything quite well … only the verification of the generated data was a bit cumbersome: Open DevTools for a page in Chrome. Search in the HEAD of the source code for the included script (“application/ld+json”) Copy out JSON-LD code Format JSON into VS code … and check Nothing for now and simply impossible on the smartphone, even if there would be a reasonable Chrome extension for displaying JSON-LD data, but it does not exist (yet). Another problem was that I use automatically generated Socal Media images for my articles, which are included in the JSON-LD, but do not appear anywhere in the page and thus were beyond my control. I simply wanted to display all the generated stuff. Since I’ve been a fan of the so-called bottom sheets since the first version of Google’s Material Design, I imagined a script that grabs the code embedded in the page and pushes a panel with all the data visualized there into the page from the bottom … and the whole act was easier than I thought. The Bottom Sheet ComponentRecently I stumbled across a small but nice bottom sheet script that is based on jQuery but that I still use on this blog itself: downupPopup.js by Ali Dinçer. It has several settings and is just about 5 KB in size, if you add the CSS code. What’s nice about it is, that all the animations that make such a component stand out, are based on said CSS and are not jQuery-driven. The bottom sheet is based on a base HTML element with a required child element: 12345<div id="myElement1"> <div class="downupPopup-content"> Lorem ipsum dolor sit amet... </div></div> This is first initialized with the desired settings, with the script adding the necessary inline styles, and then you can open and close it: 1234$("#myElement1").downupPopup();$("#myElement1").downupPopup('open');$("#myElement1").downupPopup('close'); Now my solution should work on-the-fly and without a previously defined element in the HTML … and it should be reusable, because if I already include a bottom sheet component, then I wanted to use it for future occasions. For this I wrote myself a small manager that makes different uses possible with a single call. It has one function each for a specific bottom sheet dialog and beside it base variables and functions (base) to keep the infrastructure code of the former as small as possible: dialog.js123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869var dpDialog = { 'base': { 'element': null, 'content': null, 'options': { animation: "ease", duration: 400, radiusLeft: "6px", radiusRight: "6px", width: "100%", }, 'init': function(options) { let opt = {...dpDialog.base.options, ...options}; if ($("#dpElement").length === 0) { // create new dpDialog.base.element = $(` <div id="dpElement"> <div class="downupPopup-content"></div> </div>`); dpDialog.base.element.appendTo("body"); dpDialog.base.content = dpDialog.base.element.find(".downupPopup-content"); } else { // reset existing dpDialog.base.element.downupPopup("close"); dpDialog.base.content.empty(); } dpDialog.base.element.downupPopup(opt); }, 'show': function() { setTimeout(() => { dpDialog.base.element.downupPopup("open"); }, 100); } }, 'myFirstTest': function() { // INIT DIALOG dpDialog.base.init({ headerText: "Test", distance: 75 }); // ADD CONTENT let content = ` <section> <p>Lorem ipsum dolor sit amet...</p> </section> `; $(content).appendTo(dpDialog.base.content); // OPEN DIALOG dpDialog.base.show(); }, 'pageMeta': function() { // INIT DIALOG dpDialog.base.init({ headerText: "Page Meta", contentScroll: true, distance: 6 }); // ADD CONTENT // ... appending stuff to dpDialog.base.content // OPEN DIALOG dpDialog.base.show(); }};window.dialog = dpDialog; // make it globally available The base init function takes care of initializing the downupPopup component, including dynamically inserting the necessary HTML element and attaching the desired settings. show opens up the dialog, with a small time delay, to ensure that the content has already been inserted.Dialog functions in the example above are: myFirstTest and pageMeta. Calling one of the dialog functions is simple: 1<a href="javascript:dpDialog.myFirstTest()">Open Test Dialog</a> Try here: Open Test Dialog Problem with the original implementation solvedAli decided in his original implementation to apply the given settings only once to a popup element. Once initialized, it could not be reused with different settings. To avoid having to destroy an existing element before initializing a new one, which would have caused a massive timing problem due to the animation, I decided to fork his code and give him a pull request. You can find my script here, as long as Ali didn’t include the PR in his code: kristofzerbe/downupPopup.js The PageMeta DialogNow that I had my desired display option, it was time to bring the pageMeta dialog function to life. My first thought was to use a JSON-LD parser in JavaScript provided by json-ld.org, but this is not even quickly usable, since it validates the code to be parsed at runtime against schema.org and every of my calls failed with CORS warnings. Now I didn’t want to turn this into a PhD thesis, I just wanted to display my highly customized JSON-LD, so I worked out the function quite individually. The Code ItselfI wanted to show two things in the dialog: the code itself and a visual representation of it for a better overview. Getting the content for the code was really straight forward: dialog.js123456789101112131415161718192021222324... 'pageMeta': function() { // INIT DIALOG dpDialog.base.init({ headerText: "Page Meta", contentScroll: true, distance: 6 }); // ADD CONTENT // Grab the JSON-LD code from the page let json = JSON.parse($('script[type="application/ld+json"]').text()); // Create new dialog section for the code let secCode = $('<section></section>').appendTo(dpDialog.base.content); // Append header secCode.append('<h1>JSON-LD</h1>'); // Append formatted code als PRE element secCode.append('<pre class="json">' + syntaxHighlight(JSON.stringify(json, undefined, 2))) + '</pre>'; // OPEN DIALOG dpDialog.base.show(); }... Since I had taken care to compress my JSON-LD code to save space, I now needed to get it back into a readable format. Time saver was the following script, which I found on StackOverflow (pretty-print JSON using JavaScript): 123456789101112131415161718function syntaxHighlight(json) { json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); return json.replace(/("(\\\\u[a-zA-Z0-9]{4}|\\\\[^u]|[^\\\\"])*"(\\s*:)?|\\b(true|false|null)\\b|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)/g, function (match) { var cls = 'number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'key'; } else { cls = 'string'; } } else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; } return '<span class="' + cls + '">' + match + '</span>'; });} (Stylus format)12345678910111213pre.json font-family: 'Roboto Mono',monospace font-size: 13px .string color: #4271ae .number color: #4271ae .boolean color: #4271ae .null color: #ababab .key color: #c15353 The Visual RepresentationMy JSON-LD is hierarchically structured. Each page always has a PERSON block for information about me as a person, then an ORGANIZATION block about the “people” behind the blog (just me), then a WEBSITE block for the description of the website itself and a WEBPAGE block for a single page. Article pages like this, also have an ARTICLE block and the note pages have a BLOGPOSTING block. Therefore, it seemed logical to me to display the blocks as an accordion using the DETAILS element, with only the first one open at startup. To process the required HTML, I wrote a helper function for each block that returns a string literal template to which the calling code passes the necessary data. To save space, only one block is included in the example below. The others work similarly. You can see the complete code here and modify it for your purposes: dialog.js of the blog kiko.io. dialog.js123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354... 'pageMeta': function() { // INIT DIALOG dpDialog.base.init({ headerText: "Page Meta", contentScroll: true, distance: 6 }); // ADD CONTENT // Grab the JSON-LD code from the page let json = JSON.parse($('script[type="application/ld+json"]').text()); // Create new dialog section for the visual representation let secVisual = $('<section></section>').appendTo(dpDialog.base.content); // ... other blocks for the visual representation // Block WEBSITE function getWebSite(website, organization) { return ` <details> <summary>WebSite</summary> <div> <label>Name</label> <p>${website.name}</p> <label>Description</label> <p>${website.description}</p> <label>Language</label> <p>${website.inLanguage}</p> <label>Publisher</label> <p>${organization.name}</p> </div> </details> `; } // Get WebSite block from JSON let jWebSite = json["@graph"].filter(x => x["@type"] === "WebSite"); // Get referenced Publisher information (Organization) let jPublisher = json["@graph"].filter(x => x["@id"] === jWebSite[0].publisher["@id"]); // Get filled HTML from template helper function above let tWebSite = getWebSite(jWebSite[0], jPublisher[0]); // Append HTML to content secVisual.append($(tWebSite)); // ... other blocks for the visual representation // ... (Code stuff from above) // OPEN DIALOG dpDialog.base.show(); }... ConclusionIt was fun to add a new feature to the site, even more so because it helps me keep track of the meta data of each page myself. Here (or in the footer of each page) you can see the result: Open Page Meta for this article… More Information Ali Dinçer: downupPopup.jsKristof Zerbe: Fork from downupPopup.js (Make Popup Reusable, with PR)Google: Material Design 3 - Compontents, Bottom Sheets","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Meta","slug":"Meta","permalink":"https://kiko.io/tags/Meta/"},{"name":"JSON-LD","slug":"JSON-LD","permalink":"https://kiko.io/tags/JSON-LD/"}]},{"title":"Top 10 Pens of Jon Kantner","subtitle":null,"date":"2023-05-08","updated":"2023-05-08","path":"post/Top-10-Pens-of-Jon-Kantner/","permalink":"https://kiko.io/post/Top-10-Pens-of-Jon-Kantner/","excerpt":"A while ago I posted my favourite pens of 2022. Many of them came from the pen of John Kantner and that was reason enough for me to highlight his most beautiful works in a post. My “Best of Jon Kantner” selection is focused on the usefulness for UI’s to be created, i.e. all these things I urgently need to try out or one or the other will end up in one of my projects.","keywords":"ago posted favourite pens pen john kantner reason highlight beautiful works post jon selection focused usefulness uis created things urgently end projects","text":"A while ago I posted my favourite pens of 2022. Many of them came from the pen of John Kantner and that was reason enough for me to highlight his most beautiful works in a post. My “Best of Jon Kantner” selection is focused on the usefulness for UI’s to be created, i.e. all these things I urgently need to try out or one or the other will end up in one of my projects. 1. Card-Like Menu 2. Side Navigation 3. Lotsa Notifications 4. Search Input Caret Jump 5. Sliding Stepper 6. Passcode With Sliding Cursor 7. Animated Star Rating 8. Range Sliders With a Rolling Counter 9. Colorful Theme Switch 10. Animated Upload Modal As I said, this is the list of my personal favorite works by Jon. I can only recommend everyone to check out his work on Codepen. He really is a UI rockstar …","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"UI","slug":"UI","permalink":"https://kiko.io/tags/UI/"}]},{"title":"Discoveries #24 - JavaScript & UI","subtitle":null,"series":"Discoveries","date":"2023-04-28","updated":"2023-04-28","path":"post/Discoveries-24-JavaScript-UI/","permalink":"https://kiko.io/post/Discoveries-24-JavaScript-UI/","excerpt":"I’m such a UI person. It’s a blast for me to discover and try out new interface components on the web and for the web. Simplicity and a tidy text desert is not my thing. Here are a few JavaScript UI gems I found the other day for you… Frontle - BottomSheetdownupPopup.jsPikadayAdd to Calendar ButtonTippy.jsResponsive Full Page Scrolllucafalasco/scroll-snapBgzy.jsScrollyVideo.jsCal-Heatmap","keywords":"im ui person blast discover interface components web simplicity tidy text desert thing javascript gems found day you… frontle - bottomsheetdownuppopupjspikadayadd calendar buttontippyjsresponsive full page scrolllucafalasco/scroll-snapbgzyjsscrollyvideojscal-heatmap","text":"I’m such a UI person. It’s a blast for me to discover and try out new interface components on the web and for the web. Simplicity and a tidy text desert is not my thing. Here are a few JavaScript UI gems I found the other day for you… Frontle - BottomSheetdownupPopup.jsPikadayAdd to Calendar ButtonTippy.jsResponsive Full Page Scrolllucafalasco/scroll-snapBgzy.jsScrollyVideo.jsCal-Heatmap Frontle - BottomSheet by Frontl (HyeongJun Yun) https://github.com/Frontle-Foundation/BottomSheet BottomSheet is part of the Frontl multi-platform SPA framework from South Korea and is a vanilla JavaScript implementation of the Android Bottom Sheet to show options or settings. Very neat and useful for Web Apps. downupPopup.js by Ali Dinçer https://github.com/ali-dincer/downupPopup.js Ali’s downupPopup is a wonderful UI element for replacing boring dialogs or showing additional information in a sliding panel, similar to the the previous BottomSheet. It masters HTML, forms and can be shown with a duration and full screen. Pikaday by David Bushell and Ramiro Rikkert https://github.com/Pikaday/Pikaday This date picker is not the fanciest one, but it is very lightweight, has no dependencies and is written in plain JavaScript. The ease of use and the ability to style it however you want, makes it a great little thing. Add to Calendar Button by Jens Kürschner https://add-to-calendar-button.com/ Letting the user select a date is just the first part of a process when it comes to making appointments. In the second part, the appointment must be added to the user's own calendar. This button makes it very easy in terms of Outlook, Google Calendar, Yahoo or as an ICS file. Tippy.js by James, somewhere from Australia https://atomiks.github.io/tippyjs/ Tooltip libraries really exist a lot. But Tiffy, a side project of Floating UI (now Popper), offers so many possibilities that I replace existing ones with it. Animations, Themes, Add-Ons, Plugins, and much more. Even SVG's are supported. Awesome! Responsive Full Page Scroll by Fabian Graßl https://github.com/fabeat/responsive-fullpage-scroll If you throw this library on a bit of HTML you get a wonderful a full screen scrolling page that can be activated and deactivated using a media query. Perfect for marketing pages or photo slideshows. lucafalasco/scroll-snap by Luca Falasco https://github.com/lucafalasco/scroll-snap This library helps to snap a page when user stops scrolling. It is build on top of the CSS feature scroll-snap, but offers a customizable configuration and a consistent cross browser behaviour. Bgzy.js by Nino Camdzic https://ninocamdzic.github.io/bgzy/# Visually appealing backgrounds are sometimes the salt in the soup. Of course, it depends on the photo and so that it does not look boring in the long run, Nico thought, then I'll change them in adjustable time periods with JavaScript. Great idea and nice implementation. ScrollyVideo.js by Daniel Kao https://scrollyvideo.js.org/ Parallax effects when scrolling through a website are well known by now. But the fact that you can use a video for this was new to me. What an idea! And how cool it looks in the end… Cal-Heatmap by Wan from Dubai https://cal-heatmap.com/ Anyone who has ever been on GitHub knows the heatmaps that show when and how often commits have taken place. But this kind of chart can be used for other things, Wan probably thought and made a library out of it.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Breton Presets for Lightroom","subtitle":null,"series":"Lightroom Presets","date":"2023-03-10","updated":"2023-03-10","path":"post/Breton-Presets-for-Lightroom/","permalink":"https://kiko.io/post/Breton-Presets-for-Lightroom/","excerpt":"‘Le Bretagne’ (Brittany) is one of the most beautiful and historic parts of Europe and always worth a visit. It combines the sometimes rugged English flair with the art of living of France, both scenically and in the architecture and the way people live there. My wife likes all things English and I am a friend of the french ‘Savoir Vivre’, and we were able to combine the two beautifully on a trip to Saint-Malo and Jersey. It was a photographers dream…","keywords":"le bretagne brittany beautiful historic parts europe worth visit combines rugged english flair art living france scenically architecture people live wife likes things friend french savoir vivre combine beautifully trip saint-malo jersey photographers dream…","text":"‘Le Bretagne’ (Brittany) is one of the most beautiful and historic parts of Europe and always worth a visit. It combines the sometimes rugged English flair with the art of living of France, both scenically and in the architecture and the way people live there. My wife likes all things English and I am a friend of the french ‘Savoir Vivre’, and we were able to combine the two beautifully on a trip to Saint-Malo and Jersey. It was a photographers dream… Breton GlowThe sunsets off Saint-Malo are wonderful. The water of the English Channel glows and in the sky the silhuettes of the paragliders stand out, pulling the sportsmen over the rippling water. You can sit on the waterfront for hours and still not get enough of it. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-oe1buf\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Bretagne Glow.xmp Breton SundownHaving good and clear weather here, as in the British Isles, is not a given. The evenings are all the more beautiful when it is like this and the small islands off the coast are bathed in a warm orange. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-kh4wmk\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Bretagne Sundown.xmp Breton ColorI love colors, but rarely does the camera manage to capture that magic the way I feel it. And Brittany has beautiful colors. Be it the famous Mont Saint-Michel or a cornfield … :) var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-quvh7j\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Bretagne Color.xmp Breton Sea CircleOysters from Cancale are not only famous in the region, but far beyond. A product that tastes just like the sea from which it comes looks: Awesome. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-hec5ln\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Bretagne Sea Circle.xmp Breton BeachThe beach in Saint-Malo is 3 kilometers long and it is fascinating to observe how one of the highest tides in Europe swallows it in no time and later releases it cleaned. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-fjfpvm\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Bretagne Beach.xmp","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Presets","slug":"Presets","permalink":"https://kiko.io/tags/Presets/"}]},{"title":"Discoveries #23 - UI/CSS","subtitle":null,"series":"Discoveries","date":"2023-02-12","updated":"2023-02-12","path":"post/Discoveries-23-UI-CSS/","permalink":"https://kiko.io/post/Discoveries-23-UI-CSS/","excerpt":"As a visual person, I’m always thrilled when I come across small but subtle tips, tricks or even entire solutions that lift my understanding of what can be done with CSS to new heights. Of course, this month :has() is once again one of them, but also once again contributions from Bramus van Damme and Ahmad Shadeed, whose posts I read without exception because they are both so good at what they do. Tree views in CSSScroll Shadows With JavaScriptCSS Mirror Editing in Edge DevTools for VS CodePrevent Scroll Chaining With Overscroll BehaviorDisplay content in the title bar - Microsoft Edge DevelopmentThe large, small, and dynamic viewport unitsAn Interactive Guide to Flexbox in CSSFlexbox Dynamic Line SeparatorStyle a parent element based on its number of children using CSS :has():has(): the family selector","keywords":"visual person im thrilled small subtle tips tricks entire solutions lift understanding css heights month contributions bramus van damme ahmad shadeed posts read exception good tree views cssscroll shadows javascriptcss mirror editing edge devtools codeprevent scroll chaining overscroll behaviordisplay content title bar - microsoft developmentthe large dynamic viewport unitsan interactive guide flexbox cssflexbox line separatorstyle parent element based number children hashas family selector","text":"As a visual person, I’m always thrilled when I come across small but subtle tips, tricks or even entire solutions that lift my understanding of what can be done with CSS to new heights. Of course, this month :has() is once again one of them, but also once again contributions from Bramus van Damme and Ahmad Shadeed, whose posts I read without exception because they are both so good at what they do. Tree views in CSSScroll Shadows With JavaScriptCSS Mirror Editing in Edge DevTools for VS CodePrevent Scroll Chaining With Overscroll BehaviorDisplay content in the title bar - Microsoft Edge DevelopmentThe large, small, and dynamic viewport unitsAn Interactive Guide to Flexbox in CSSFlexbox Dynamic Line SeparatorStyle a parent element based on its number of children using CSS :has():has(): the family selector Tree views in CSS by Kate Rose Morley https://iamkate.com/code/tree-views/ Kate shows us how to create a tree view as collapsible list, created using only html and css, without the need for JavaScript Scroll Shadows With JavaScript by Chris Coyier https://css-tricks.com/scroll-shadows-with-javascript/ A good scrollable design shows the user if he can scroll further or not. Chris has an approach on that with pure CSS. CSS Mirror Editing in Edge DevTools for VS Code by Christian Heilmann https://christianheilmann.com/2021/09/16/css-mirror-editing-in-edge-devtools-for-vs-code/ How often do you fiddle around with CSS in Chrome's DevTools and copy the stuff back to your code? Christian shows how Mirror Editing works. Prevent Scroll Chaining With Overscroll Behavior by Ahmad Shadeed https://ishadeed.com/article/prevent-scroll-chaining-overscroll-behavior/ Dealing with scroll boundaries when you have many scrolling boxes on a page is a mess, until you have read Ahmad's advice regarding the use of 'overscroll-behavior' Display content in the title bar - Microsoft Edge Development by Microsoft Learn https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/window-controls-overlay In PWAs, at least on the desktop, a lot of space is wasted with the title bar of the window. The use of 'display_override' should change that. The large, small, and dynamic viewport units by Bramus Van Damme https://web.dev/viewport-units/ The most used device on the Internet has long been the smartphone, but the visible area is trimmed by the browsers there from the necessary dynamic toolbars. To address this, there are new size units. An Interactive Guide to Flexbox in CSS by Josh Comeau https://www.joshwcomeau.com/css/interactive-guide-to-flexbox/ There are plenty of Flexbox tutorials, cheat sheets and generators, but Josh turns it into an interactive learning lesson. Very memorable. Flexbox Dynamic Line Separator by Ahmad Shadeed https://ishadeed.com/article/flexbox-separator/ Flexbox again and Ahmad again … If you need separator lines between boxes for different devices, here's how the can be done nice and easy. Style a parent element based on its number of children using CSS :has() by Bramus Van Damme https://www.bram.us/2022/11/17/style-a-parent-element-based-on-its-number-of-children-using-css-has/ :has() is the hottest kid in town right now, because it allows the long-cherished dream of many web developers to style a parent element depending on his children. Bramus shows how… :has(): the family selector by Jhey Tompkins https://developer.chrome.com/blog/has-m105/ As :has() is so hot, it's good to have another resource talking about. Jhey has collected so many examples that hardly any questions remain open.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Provide Blog Metadata via JSON-LD","subtitle":"Centralization of a website's schema.org data in the HEAD instead of everywhere in the HTML","series":"A New Blog","date":"2023-02-09","updated":"2023-02-09","path":"post/Provide-Blog-Metadata-via-JSON-LD/","permalink":"https://kiko.io/post/Provide-Blog-Metadata-via-JSON-LD/","excerpt":"Chris Coyier’s post “Open Graph Blues“ got me thinking that my blog’s metadata, which are used by Google among others to index my pages, aren’t really at the cutting edge anymore. I took the markup of the individual elements of the pages via schema.org Microdata attributes from the standard Hexo template years ago and always adjusted it by value, but never questioned that there are more modern variants to provide the metadata. It’s Ok for Google to use Microdata attributes, but the HTML code of my templates is getting more and more opaque, because next to these stick to the tags also those for the Indieweb, classes for CSS and last but not least those for the own indexing via Pagefind. There becomes from a simple 1234<article> <h1>Title of my latest blog post</h1> <p> ... </p></article> quickly becomes a … 123456<article class="article-type-post h-entry" data-pagefind-body="" data-pagefind-meta="type:Article" itemscope itemprop="blogPost"> <h1 class="article-title p-name" itemprop="name">Title of my latest blog post</h1> <p> ... </p></article> Lots of textual overhead and the hardest part is maintaining it over the long term. Better would be a complete search engine description in the header of a page, where also the other meta information is available. In one place and not spread all over the HTML code. JSON-LD to the rescue…","keywords":"chris coyiers post open graph blues thinking blogs metadata google index pages arent cutting edge anymore markup individual elements schemaorg microdata attributes standard hexo template years ago adjusted questioned modern variants provide html code templates opaque stick tags indieweb classes css indexing pagefind simple 1234<article> <h1>title latest blog post</h1> <p> </p></article> quickly … 123456<article class="article-type-post h-entry" data-pagefind-body="" data-pagefind-meta="typearticle" itemscope itemprop="blogpost"> <h1 class="article-title p-name" itemprop="name">title lots textual overhead hardest part maintaining long term complete search engine description header page meta information place spread json-ld rescue…","text":"Chris Coyier’s post “Open Graph Blues“ got me thinking that my blog’s metadata, which are used by Google among others to index my pages, aren’t really at the cutting edge anymore. I took the markup of the individual elements of the pages via schema.org Microdata attributes from the standard Hexo template years ago and always adjusted it by value, but never questioned that there are more modern variants to provide the metadata. It’s Ok for Google to use Microdata attributes, but the HTML code of my templates is getting more and more opaque, because next to these stick to the tags also those for the Indieweb, classes for CSS and last but not least those for the own indexing via Pagefind. There becomes from a simple 1234<article> <h1>Title of my latest blog post</h1> <p> ... </p></article> quickly becomes a … 123456<article class="article-type-post h-entry" data-pagefind-body="" data-pagefind-meta="type:Article" itemscope itemprop="blogPost"> <h1 class="article-title p-name" itemprop="name">Title of my latest blog post</h1> <p> ... </p></article> Lots of textual overhead and the hardest part is maintaining it over the long term. Better would be a complete search engine description in the header of a page, where also the other meta information is available. In one place and not spread all over the HTML code. JSON-LD to the rescue… Structured Meta DataGoogle has published tons of information in its Search Central on how to place metadata on your page to be found more easily in the index. You can also see that they are maintained by the update date of individual pages, for example “Last updated 2023-01-26 UTC“. End of last week. That’s up to date, fine. Of course, they also show how to use Microdata, but recommended is the use of JSON-LD, a structured and centralized inclusion of the required information via a SCRIPT tag in the header of the page. Thereby information about the website in general, the author, the organization behind it and the actual article page can be combined separately in one piece of JSON-LD code. Google’s solution is based on schema.org, but they have picked only what is necessary for them, which means: they deal only with a subset of the schema.org types. Since it is somewhat cumbersome to write correct JSON-LD by hand, there are of course online editors for it, e.g. within the web code tools or Merkle. But these generators unfortunately do not map all the possibilities and useful entries, so they can only generate general templates to be elaborated. Moreover, you can use the JSON-LD code from this article as a basis for your solution, because it covers the most important aspects. In the following I first describe the general content of the individual JSON-LD blocks and then how to assemble them, so that it makes sense for the search engine. AuthorFirst of all, this code is about me myself and I… 123456789101112{ "@type": "Person", "@id": "https://kiko.io/#person", "name": "Kristof Zerbe", "url": "https://kiko.io/about", "image": "https://kiko.io/images/kristof-zerbe.png", "sameAs": [ "https://indieweb.social/@kiko", "https://github.com/kristofzerbe", "https://500px.com/p/kikon" ] } It is advisable to include so called Node Identifiers (@id) in order to reuse certain information later on as a reference and prevent repeating data. These identifiers are canonical URL/URI representations. OrganizationMost blogs are run by individuals and not necessarily by organizations, so you might think this area would not be interesting, but it is for a reason: only here you can deposit the link to a logo of your blog, which can then be displayed in the search. 1234567{ "@type": "Organization", "@id": "https://kiko.io/#organization", "name": "kiko.io", "url": "https://kiko.io", "logo": "https://kiko.io/images/apple-touch-icon.png"} WebsiteThe JSON-LD block related to this website itself looks like this: 12345678910111213141516171819{ "@type": "WebSite", "@id": "https://kiko.io/#website", "url": "https://kiko.io", "name": "kiko.io", "description": "Blog about memorable tech stuff by Kristof Zerbe", "inLanguage": "en-US", "publisher": { "@id": "https://kiko.io/#organization" }, "potentialAction": { "@type": "SearchAction", "target": { "@type": "EntryPoint", "urlTemplate": "https://kiko.io/search/?q={searchTerm}" }, "query-input": "required name=searchTerm" }} Remarkable in this block is the potentialAction, which specifies the possibility to let the search engine (Google, whatelse) integrate a Sitelinks Search Box, a search box inside the result list, as described here. There is a shorthand format for this and some generators like Merkle are using it, but it is not recommended, because it’s non-standard. (Page) ImagesI use my own photographs on every page as header images and to provide some additional information on these, there is a JSON-LD block for the image I use. 1234567891011121314{ "@type": "ImageObject", "@id": "https://kiko.io#photo/D70_9216", "caption": "Broken Onion", "url": "https://kiko.io/photos/normal/D70_9216.jpg", "contentUrl": "https://kiko.io/photos/normal/D70_9216.jpg", "license": "https://creativecommons.org/licenses/by-sa/4.0/", "acquireLicensePage": "https://kiko.io/photos", "creditText": "Kristof Zerbe", "copyrightNotice": "Kristof Zerbe (CC BY-SA 4.0)", "creator": { "@id": "https://kiko.io/#person" }} For better recognition of my posts (currently articles only), I generate a special image for each post for the social media platforms. It is based on the photo associated with the post and includes it’s title and subtitle in addition to the logo. How I generate these things can be read in my post Generate Social Media Images Automatically. For this image, there is a second JSON-LD block that I can reference later on: 123456789{ "@type": "ImageObject", "@id": "https://kiko.io#image/Provide-Blog-Metadata-via-JSON-LD", "url": "https://kiko.io/images/social-media/Provide-Blog-Metadata-via-JSON-LD.png", "contentUrl": "https://kiko.io/images/social-media/Provide-Blog-Metadata-via-JSON-LD.png", "creator": { "@id": "https://kiko.io/#person" }} WebpageThe previously described JSON-LD blocks are basically just a preparation for the description of the individual pages that will be indexed by the search engine. The following block now describes a page itself and includes the previously described blocks by referencing the @id: 1234567891011121314151617{ "@type": "WebPage", "@id": "https://kiko.io/post/Provide-Blog-Metadata-via-JSON-LD", "url": "https://kiko.io/post/Provide-Blog-Metadata-via-JSON-LD", "name": "Provide Blog Metadata via JSON-LD", "isPartOf": { "@id": "https://kiko.io/#website" }, "description": "Centralization of a website's schema.org data in the HEAD instead of everywhere in the HTML", "inLanguage": "en-US", "primaryImageOfPage": { "@id": "https://kiko.io#photo/D70_9216" }, "image": { "@id": "https://kiko.io#photo/D70_9216" }} ArticleIn addition to the previous description of a web page, the following block is a more detailed description of the page as an article or blog post. 12345678910111213141516171819{ "@type": "Article", "@id": "https://kiko.io/post/Provide-Blog-Metadata-via-JSON-LD#article", "mainEntityOfPage": { "@id": "https://kiko.io/post/Provide-Blog-Metadata-via-JSON-LD" }, "headline": "Provide Blog Metadata via JSON-LD", "datePublished": "2023-02-02T12:00:00+00:00", "dateModified": "2023-02-02T12:00:00+00:00", "image": { "@id": "https://kiko.io#image/Provide-Blog-Metadata-via-JSON-LD" }, "author": { "@id": "https://kiko.io/#person" }, "publisher": { "@id": "https://kiko.io/#organization" }} As described earlier, the identifiers defined in the other blocks are used for referencing the author and the publisher, as I did it in the block website. At first glance, this information appears to be duplicated and I am currently not sure if it is really needed.mainEntityOfPage makes clear, that this article is the main entity for that web page.The image of the article will be the social media image defined in the second ImageObject block. Assembling the blocksGenerally, JSON-LD data is included in a page via a script tag. You can either output each individual block separately or together within a so called graph. In both cases, the leading specification of the context is necessary. Separate 12345678<script type="application/ld+json"> { "@context": "http://schema.org/", ... rest of the block }</script><script type="application/ld+json"> ... </script>... Together 123456789<script type="application/ld+json"> { "@context": "http://schema.org/", "@graph": [ ... all needed blocks in hierarchy ] }</script> Since this is probably the most common and also the most space-saving way, I have chosen the graph. When arranging the blocks it is useful to keep the hierarchy, from specific to general. Here is a schematic example of an article: 123456789101112131415<script type="application/ld+json"> { "@context": "http://schema.org/", "@graph": [ { ... Article }, { ... WebPage }, { ... ImageObject (Social Media Image) }, { ... ImageObject (Photo) }, { ... WebSite }, { ... Organization }, { ... Person } ] }</script> On all pages that are not an article, of course, the Article and the ImageObject (Social Media Image) blocks are not necessary. Here is the sample for an ordinary page: 12345678910111213<script type="application/ld+json"> { "@context": "http://schema.org/", "@graph": [ { ... WebPage }, { ... ImageObject (Photo) }, { ... WebSite }, { ... Organization }, { ... Person } ] }</script> GenerationSince my blog is based on SSG Hexo, I have all the data and capabilities to have the JSON-LD data of a page automatically generated. I don`t want to go into too much depth here about how I implemented this, but in general there is an EJS file for each block that renders the required JSON-LD code via the available configuration and page data stored and passed in a META object. Through various wrappers, these are then included in the head.ejs. I have currently three of them: json-ld-page.ejs … for all pages, except the other two below json-ld-article.ejs … for articles (normal kiko.io Posts) json-ld-blogposting.ejs … for notes (see kiko.io Notes) json-ld-article.ejs12345678910111213{ "@context": "http://schema.org/", "@graph": [ <%- partial('_partial/meta/_article') %>, <%- partial('_partial/meta/_webpage') %>, <%- partial('_partial/meta/_image') %>, <%- partial('_partial/meta/_photo') %>, <%- partial('_partial/meta/_website') %>, <%- partial('_partial/meta/_organization') %>, <%- partial('_partial/meta/_author') %> ]} head.ejs12345678<%#!-- JSON-LD (schema.org) for Google --%><% let jsonPartial = "json-ld-" + meta.type; let jsonLD = partial('_partial/meta/' + jsonPartial, { meta: meta }); jsonLD = JSON.stringify(JSON.parse(jsonLD));%><script type="application/ld+json"><%- jsonLD %></script> The last line in the JavaScript ensures, that the produced JSON is compacted to one single line … easy, by converting the string into an object and back to a string. Test the JSON-LDWhen you have everything together, it is advisable to test the resulting code. Schema.org offers such a tool at https://validator.schema.org/. In addition, there is the Google Rich Results Test, which validates your code against the partially specific implementation for their own search engine. ConclusionIt may be a few bytes more that are delivered to the user in the browser or the search engine bot, but the advantage is, that all the information describing the page is stored in one place in the header of the page and nothing is scattered all over the HTML anymore. Maintenance of both the code and the meta data is made much easier as a result. More Information Google Search Central: Introduction to structured data markup in Google SearchPatrick Coombe and Craig Mount: Steal Our JSON-LDAndrew Welch: Annotated JSON-LD Structured Data ExamplesAlberto Carniel: Schema markup and structured data ultimate guide (JSON-LD)Brian Gorman: An SEO’s Guide to Writing Structured Data (JSON-LD)Merkle: Schema Markup Generator (JSON-LD)webcode.tools: Generators > Structured Data","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Search","slug":"Search","permalink":"https://kiko.io/tags/Search/"},{"name":"JSON-LD","slug":"JSON-LD","permalink":"https://kiko.io/tags/JSON-LD/"}]},{"title":"Radio Garden","subtitle":"Free, registration-free and simply cool - Travel through the radio stations of the world with Radio Garden","series":"Golem","date":"2023-02-01","updated":"2023-02-01","path":"post/Radio-Garden/","permalink":"https://kiko.io/post/Radio-Garden/","excerpt":"In the age of streaming services, radio may seem out of date, but it still surrounds us constantly, even if we often hardly notice it - in the car, on the public transport, at work or simply at home in the kitchen. One strength of the old medium is that it presents us with music we haven’t heard before, away from our playlists on Spotify or iTunes. The easiest way to rediscover new artists or entire genres of music is to pick a radio station at random. It’s precisely this lack of control, the being at the mercy, the sometimes unpredictable that makes the medium so appealing to many - and with digital streaming, many of the world’s stations are just a tap away. As is so often the case in the modern world, it’s the oversupply that leaves some confused and frustrated. You first have to be able to pick out what you might like from the gigantic haystack of options. For this purpose, there are little helpers that more or less independently suggest what you might listen to. One of them stands out from the crowd because, on the one hand, it does not pursue commercial interests and, on the other hand, it approaches the station search very intuitively: Radio Garden (https://radio.garden).","keywords":"age streaming services radio date surrounds constantly notice - car public transport work simply home kitchen strength medium presents music havent heard playlists spotify itunes easiest rediscover artists entire genres pick station random precisely lack control mercy unpredictable makes appealing digital worlds stations tap case modern world oversupply leaves confused frustrated gigantic haystack options purpose helpers independently suggest listen stands crowd hand pursue commercial interests approaches search intuitively garden (https://radio.garden).","text":"In the age of streaming services, radio may seem out of date, but it still surrounds us constantly, even if we often hardly notice it - in the car, on the public transport, at work or simply at home in the kitchen. One strength of the old medium is that it presents us with music we haven’t heard before, away from our playlists on Spotify or iTunes. The easiest way to rediscover new artists or entire genres of music is to pick a radio station at random. It’s precisely this lack of control, the being at the mercy, the sometimes unpredictable that makes the medium so appealing to many - and with digital streaming, many of the world’s stations are just a tap away. As is so often the case in the modern world, it’s the oversupply that leaves some confused and frustrated. You first have to be able to pick out what you might like from the gigantic haystack of options. For this purpose, there are little helpers that more or less independently suggest what you might listen to. One of them stands out from the crowd because, on the one hand, it does not pursue commercial interests and, on the other hand, it approaches the station search very intuitively: Radio Garden (https://radio.garden). Indian esotericism, Estonian metalThe tool, which works very well in any modern browser, but is also available as an Android or iOS app, initially shows nothing more than an interactive globe on which tens of thousands of green dots whiz by under a round target cursor when you move the map back and forth or zoom into the world. If you set your sights on one of the green dots, the station for that city is loaded and played. If there are several stations in one place, the dot appears larger and you are also offered a station list. This is so intuitive that it instantly awakens the spirit of discovery and in a few minutes you can listen to Japanese, Australian or Peruvian radio stations and decide whether to add them to your list of favorites. Using the search function, you can find not only your local, familiar stations (if they offer an Internet stream), but also thematic stations. The directory even includes some US police stations that stream their radio traffic on the Internet. Radio Garden grew out of a scholarly project by the Netherland Institute for Sound and Vision in Hilversum, the Netherlands, which between 2013 and 2016 studied what radio sounds like in other parts of the world and what borders, different cultural identities, and encounters do to the medium. The British Jonathan Puckey and his studio played a key role in the project and its technical implementation. In several iteration stages, this resulted in the web app radio.garden, which Puckey is still in charge of today. Instead of maps, the team from Amsterdam used satellite images from the beginning to illustrate that radio signals have always had the power to cross borders. Radio Garden is free, and you don’t need to register, log in, or even provide an email address. Nothing. Open up, select a station, listen to the radio. tns({ container: \"#image-slide-qbz2dh\", items: 1, slideBy: \"page\", controls: false, nav: true }); Unfortunately, there are a few regions in the world that have limited access to the radio.garden website or apps. For example, in the replies to a Mastodon post on the topic, there were some comments from people in the UK who could not access a station outside their island. This problem seems to have a licensing background and thus be a Brexit impact, but there are unfortunately no official statements on this. Similarly in Turkey, where the service was discontinued after the Radio and Television Supreme Council there ordered the operator to make license payments, according to Wikipedia. The only way around these restrictions is to use a VPN in another country. It’s a lot of fun to roam around the globe and try to follow an Indonesian news broadcast, guess which products the advertisements of a Greek station are trying to sell, let yourself be carried away to other worlds by the spherical sounds of an Indian esoteric station, or really shake your hair to the beat of the hard metal sounds of an Estonian rock station. Radio Garden is a very exciting project, wonderfully presented.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Audio","slug":"Audio","permalink":"https://kiko.io/tags/Audio/"}]},{"title":"Pagefind UI and URL Parameters","subtitle":null,"series":"A New Blog","date":"2023-01-31","updated":"2023-02-03","path":"post/Pagefind-UI-and-URL-Parameters/","permalink":"https://kiko.io/post/Pagefind-UI-and-URL-Parameters/","excerpt":"UPDATE: Parts of the original post are outdated, as Pagefind DOES offer a way to preset a search string, which just hasn’t been documented yet … \\o/ … see below. A couple of days ago I wrote about my attempt to integrate Pagefind in my blog. In the meantime, I further refined the indexing by excluding more content areas and adding more for the metadata to make the search results even better. But one thing was still missing: controlling the search via url parameters, so that you can actually consider the page as a search page. I came across this in a post about the sense and nonsense of Open Graph attributes and other search engine related metadata nowadays. Google, for example, likes to use JSON-LD and when describing the site you can define a search page which then makes it easier to search the site directly via Google … see Sitelinks search box (WebSite) structured data In my implementation, I decided to adapt the Pagefind UI for myself instead of developing everything from scratch via JavaScript. There are always some limitations with pre-built solutions, but I want to show here that they are actually none for the inclusion of a url parameter.","keywords":"update parts original post outdated pagefind offer preset search string hasnt documented … \\o/ couple days ago wrote attempt integrate blog meantime refined indexing excluding content areas adding metadata make results thing missing controlling url parameters page sense nonsense open graph attributes engine related nowadays google likes json-ld describing site define makes easier directly sitelinks box website structured data implementation decided adapt ui developing scratch javascript limitations pre-built solutions show inclusion parameter","text":"UPDATE: Parts of the original post are outdated, as Pagefind DOES offer a way to preset a search string, which just hasn’t been documented yet … \\o/ … see below. A couple of days ago I wrote about my attempt to integrate Pagefind in my blog. In the meantime, I further refined the indexing by excluding more content areas and adding more for the metadata to make the search results even better. But one thing was still missing: controlling the search via url parameters, so that you can actually consider the page as a search page. I came across this in a post about the sense and nonsense of Open Graph attributes and other search engine related metadata nowadays. Google, for example, likes to use JSON-LD and when describing the site you can define a search page which then makes it easier to search the site directly via Google … see Sitelinks search box (WebSite) structured data In my implementation, I decided to adapt the Pagefind UI for myself instead of developing everything from scratch via JavaScript. There are always some limitations with pre-built solutions, but I want to show here that they are actually none for the inclusion of a url parameter. My Pagefind search page is accessible at /search and therefore it’s easy to provide with parameters, f.e. /search/?q=test. Retrieving them on the page via JavaScript is no rocket science either: Search Page12345678910111213<script> // get value search parameter const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const searchString = urlParams.get("q"); // initialize Pagefind UI window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({ element: "#search" }); });</script> Now Pagefind does not offer the possibility to initialize the search on the page already with a certain value, which would be the easiest way. You can only insert the value supplied via the URL parameter into the initialized INPUT field **afterwards** and ensure that the search is triggered with it. Unfortunately, Pagefind also does not provide a callback method to do things after successful initialization. So, my implementation needs a \"guard\" that kicks in as soon as the INPUT field is ready for a search string to be entered. For this I use the following small function called ``waitForElm``, which uses JS's ``MutationAbserver`` and is located in my *tools.js* file. It triggers a Promise as soon as an element is available on the page. Update As I learned after creating an issue (#214) in Pagefind’s GitHub repo, there IS a way to preset the incoming search string by using the method triggerSearch, but as Liam pointed out it is not yet documented. But I still need the following function called waitForElm to set the focus into the created INPUT … but there is also an existing issue (#121) for this focus feature, so let’s see how long I need the function at all. tools.js12345678910111213141516171819function waitForElm(selector) { return new Promise((resolve) => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver((mutations) => { if (document.querySelector(selector)) { resolve(document.querySelector(selector)); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true, }); });} With this function and the name of the INPUT field that Pagefind inserts into the ``#search`` wrapper during initialization, the URL parameter can now be set. Since Pagefind already shows results when typing the first characters, the easiest way to trigger the search after setting the value is by dispatching the ``input`` event. Lets see how to implement Pagefind’s triggerSearch function, which is automatically delayed until the search is loaded and ready, if there is an incoming search string: Search Page123456789101112131415161718192021<script> // get value search parameter const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const searchString = urlParams.get("q"); // initialize Pagefind UI window.addEventListener('DOMContentLoaded', (event) => { let pagefind = new PagefindUI({ element: "#search" }); if (searchString) { pagefind.triggerSearch(searchString); } }); // setting the focus into the generated INPUT field as it appears waitForElm(".pagefind-ui__search-input").then((elm) => { elm.focus(); }); </script> You can try my solution here: /search/?q=pagefind","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"},{"name":"Search","slug":"Search","permalink":"https://kiko.io/tags/Search/"}]},{"title":"Integration of Pagefind in Hexo","subtitle":"Adding a low-bandwidth local search to a static Hexo-powered website","series":"A New Blog","date":"2023-01-19","updated":"2023-01-19","path":"post/Integration-of-Pagefind-in-Hexo/","permalink":"https://kiko.io/post/Integration-of-Pagefind-in-Hexo/","excerpt":"From the beginning of this blog I wanted to provide some kind of full text search in order to give my users the ability to find stuff by keyword. There are a few Hexo plugins that have approached the subject, but it was not really satisfactory and performant. So I relied on the worlds biggest search engine: Google. A search button, which drives out a small input field and with the pressing of the ENTER key the form was sent via GET to //google.com/search. The procedure was simple, but also burdened with the fact that I always expose my users to Google. At least until now … :) Bryce Wray set me on a new path with his post Sweeter searches with Pagefind, in which he talks about his experience with the still fairly fresh tool Pagefind… Pagefind is a fully static search library that aims to perform well on large sites, while using as little of your users’ bandwidth as possible, and without hosting any infrastructure … --- Liam Bigelow @ pagefind.app Pardon me? A full text search for SSG’s running completely in the browser? It sounded so great that I had to try it right away. And what can I say … it not only works fantastically well, but is also extremely easy to implement. Of course, you have to consider a few things, especially with regard to the SSG Hexo I use, but I didn’t find any big hurdles, also because the tool is so well documented. Let’s see what my implementation looks like…","keywords":"beginning blog wanted provide kind full text search order give users ability find stuff keyword hexo plugins approached subject satisfactory performant relied worlds biggest engine google button drives small input field pressing enter key form //googlecom/search procedure simple burdened fact expose … bryce wray set path post sweeter searches pagefind talks experience fairly fresh tool pagefind… fully static library aims perform large sites bandwidth hosting infrastructure --- liam bigelow pagefindapp pardon ssgs running completely browser sounded great works fantastically extremely easy implement things regard ssg didnt big hurdles documented lets implementation like…","text":"From the beginning of this blog I wanted to provide some kind of full text search in order to give my users the ability to find stuff by keyword. There are a few Hexo plugins that have approached the subject, but it was not really satisfactory and performant. So I relied on the worlds biggest search engine: Google. A search button, which drives out a small input field and with the pressing of the ENTER key the form was sent via GET to //google.com/search. The procedure was simple, but also burdened with the fact that I always expose my users to Google. At least until now … :) Bryce Wray set me on a new path with his post Sweeter searches with Pagefind, in which he talks about his experience with the still fairly fresh tool Pagefind… Pagefind is a fully static search library that aims to perform well on large sites, while using as little of your users’ bandwidth as possible, and without hosting any infrastructure … --- Liam Bigelow @ pagefind.app Pardon me? A full text search for SSG’s running completely in the browser? It sounded so great that I had to try it right away. And what can I say … it not only works fantastically well, but is also extremely easy to implement. Of course, you have to consider a few things, especially with regard to the SSG Hexo I use, but I didn’t find any big hurdles, also because the tool is so well documented. Let’s see what my implementation looks like… The Tool in BriefPagefind it is a Node.js tool and is started via the Node Package Runner (npx) and runs against the static files already created during the build. It indexes all the desired pages or even parts of the pages and creates meta and index files for them in a special build folder, which can be retrieved later via JavaScript. To make things a bit more user-friendly, Pagefind also directly generates the necessary JavaScript and CSS files for a UI. … but Liam can explain better how it works: Implementation in HexoFirst of all I decided to store all necessary parameters in a supported config file in the root of my blog project. pagefind.yml123456source: docsbundle_dir: pagefindexclude_selectors: - ".note-list" - ".anything-list" - ".article-related" source defines the relative folder where all static files are created during the build and which should now be indexed. bundle_dir overrides the default storage folder called _pagefind, which is created in the build folder for the search files. This is necessary because my blog is built and hosted on Github Pages and the responsible GitHub Action goes over folders with a starting underscore on deployment. More info on that here and here. exclude_selectors is a list of all those page elements whose content should NOT be indexed, but more about that later. With another setting called glob it is possible to tell Pagefind which files to index, but this currently has its pitfalls when trying to exclude some. Liam already has this on the screen for one of the next versions. Limiting Indexing ContentA post on a web page never stands alone, but is surrounded by other elements such as navigation, further links, etc. However, these addional elements should not end up in the index. Pagefind skips some of them like nav, form or script automatically, but there always remain some that should be excluded by hand. Best option to narrow down the indexable content is the use of the attribute data-pagefind-body. Instead of excluding something, tell Pagefind what to include. However, this approach makes it easier, but also has consequences: If data-pagefind-body is found anywhere on your site, any pages without this attribute will be removed from your index. --- pagefind.app In my case, I had a few places in my templates that I needed to add the attribute to: Type File Element Post _partial/article.ejs article Notes note.ejs article Page page.ejs .page-content Dynamic Page [name].ejs .page-content Anything Page _partial/anything-page-item.ejs .anything-content All elements inside of the elements attributed like that, I had to exclude via the setting exclude_selectors in the config (see above). Specifying Meta InformationIt was important to me to show the date of a post in the search result, because nothing is as inaccurate as a post that is many years old. With Pagefind you select the HTML element in the templates in which the meta value is located and attribute it with data-pagefind-meta, for example: date.ejs1234<time class="published dt-published" itemprop="datePublished" data-pagefind-meta="date"> <%= date(page.date, 'DD MMM YYYY') %></time> As title for the search hit Pagefind searches for H1 tags and takes the value of the last tag it finds. If you are not sure that there is always only one H1 tag on the page (and for me it is), then you better specify which tag it should take: title.ejs123<h1 class="<%= class_name %>" itemprop="name" data-pagefind-meta="title"> <%= post.title %></h1> Thus, on specifying meta data you can refer not only to the content of a tag, but also to other attributes. Here’s the example for my special Hexo implementation for header images: photograph.ejs1234<img id="header-photo" data-pagefind-meta="image[src], image_alt[alt]" src="/photos/normal/<%= page.photograph.file %>" alt="<%= page.photograph.name%>" width="0" height="0"/> In case there is simply no element that contains the meta value, you can also specify it within the attribute: article.ejs12345<article id="note-<%= page.slug %>" itemprop="blogPost" data-pagefind-body data-pagefind-meta="type:Note"> ...</article> Adding a Search PagePagefind includes not only everything to get search results from the created index by JavaScript, but also the complete UI for a search page, which means that you can build the complete search into your site yourself, or simply take a pre-built UI and visually customize it if necessary. I did the latter. The following code is the basic structure of the search page as suggested by Pagefind: 123456789<link href="/pagefind/pagefind-ui.css" rel="stylesheet"><script src="/pagefind/pagefind-ui.js" type="text/javascript"></script><div id="search"></div><script> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({ element: "#search" }); });</script> To accommodate this in Hexo, it is advisable to use your own template and generate the page with an appropriate generator. A standard PAGE in Markdown format is only conditionally suitable for this, because links and scripts are needed. I described how to implement such a generator that renders descriptive Markdown in addition to the EJS template in my post Pattern for dynamic Hexo pages, and I’ve taken that approach here as well. For simplicity, I won’t list the full code here, but link to my blog’s GitHub repo: File Description /source/_dynamic/search.md Markdown file with Frontmatter data and introduction text /themes/landscape/layout/search.ejs Layout template for search page /themes/landscape/script/generator-dynamic-search.js Hexo Generator for creating the page during build /themes/landscape/script/source/css/_pagefind.styl Customized CSS Variables and style overrides For the visual customization of the user interface Pagefind provides some CSS variables in the automatically generated CSS file. These help a bit to customize the UI to your own ideas, but I decided to override some of the styles in a seperate file called _pagefind.styl, which will be bundled via @import "_pagefind" in the main styles.styl. Since the main bundled CSS file is loaded in the HEAD before the _pagefind.css somewhere in the page, for simplicity I first made sure to pull the overrides with !important. This is not yet pretty and I will have to revise this later on. Running Build and PagefindThus prepared, the rest is a piece of cake. Pagefind does not need to be installed, because if you call the npm package via npx, the latest version will be downloaded and executed automatically. You just have to make sure that the hexo build has run before. The best way is to run the following command: hexo clean && hexo generate &&npx pagefind This my result in the console: 123456789101112131415161718192021222324Running Pagefind v0.10.7 (Extended)Running from: "...\\\\kiko.io"Source: "docs"Bundle Directory: "pagefind"[Walking source directory]Found 319 files matching **/*.{html}[Parsing files]Found a data-pagefind-body element on the site.↳ Ignoring pages without this tag.[Reading languages]Discovered 1 language: en[Building search indexes]Total: Indexed 1 language Indexed 124 pages Indexed 7220 words Indexed 0 filters Indexed 0 sortsFinished in 5.924 seconds Mount in GitHub ActionSince I am hosting this blog on GitHub Pages and the complete build and deployment is done by a GitHub Action, I added a step to the hexo-build job in the workflow file so that after the build Pagefind indexes the result: hexo-build.yml12345678910jobs: hexo-build: runs-on: ubuntu-latest steps: ... - name: Build run: npm run generate - name: Run Pagefind run: npm_config_yes=true npx pagefind ... Thankfully, in his article on Pagefind, Bryce also put me on the track of how to prevent a possible security prompt caused by npx from blocking Pagefind to run … npm_config_yes=true. The ResultThe finished solution is really amazing. As soon as you start typing, the included Pagefind JavaScript updates the results list … and it’s sooo fast. Really an exciting tool. Thanks to Liam and CloudCannon. I hope that my explanation has inspired you to try it out for yourself on your Hexo driven blog or website. If you need some help or advice, drop me a line… More Info CloudCannon: PagefindLiam Bigelow: Introducing Pagefind: Static Low-bandwidth Search at ScaleBryce Wray: Pagefind is quite a find for site searchBryce Wray: Sweeter searches with PagefindNicolas Deville: Pagefind","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"},{"name":"Search","slug":"Search","permalink":"https://kiko.io/tags/Search/"}]},{"title":"Favourite Pens of 2022","subtitle":null,"date":"2023-01-14","updated":"2023-01-14","path":"post/Favourite-Pens-of-2022/","permalink":"https://kiko.io/post/Favourite-Pens-of-2022/","excerpt":"Every year CodePen publishes a list of the 100 most “liked” pens on their site: The Most Hearted of 2022. It’s always exciting to scroll through the list and marvel at the incredibly good work of CSS artists. My favorite in terms of CSS coding art this year is the work A moment of pure CSS by Ben Evans. Absolutely amazing what he does with pure CSS: From all these works you can take a lot of know-how for yourself, but many of these pens have no practical use at first, i.e. you can’t really use them directly on your own website. They are art. Some of them impressed me not only because of their creativity, but I saved them on my own Trello list to try them out in one of the next projects. Partly they are clever approaches regarding usability, partly more or less standard functions were implemented in a visually impressive way. Some of them need some JS to work, sime of them not. Let yourself be inspired …","keywords":"year codepen publishes list pens site hearted exciting scroll marvel incredibly good work css artists favorite terms coding art moment pure ben evans absolutely amazing works lot know-how practical directly website impressed creativity saved trello projects partly clever approaches usability standard functions implemented visually impressive js sime inspired …","text":"Every year CodePen publishes a list of the 100 most “liked” pens on their site: The Most Hearted of 2022. It’s always exciting to scroll through the list and marvel at the incredibly good work of CSS artists. My favorite in terms of CSS coding art this year is the work A moment of pure CSS by Ben Evans. Absolutely amazing what he does with pure CSS: From all these works you can take a lot of know-how for yourself, but many of these pens have no practical use at first, i.e. you can’t really use them directly on your own website. They are art. Some of them impressed me not only because of their creativity, but I saved them on my own Trello list to try them out in one of the next projects. Partly they are clever approaches regarding usability, partly more or less standard functions were implemented in a visually impressive way. Some of them need some JS to work, sime of them not. Let yourself be inspired … 1. iOS Notifications by Yoav Kadosh 2. Lotsa Notifications by Jon Kantner 3. Animated Star Rating by Jon Kantner 4. Changing Face Rating by Jon Kantner 5. CSS Minimal Dark Mode Toggle Button by Greg Vissing 6. Menu micro-interaction with CSS by Mert Cukuren 7. Animated BottomBar Experiment by Chris Bautista 8. Progress Button by Taylon, Chan 9. Gradient background with waves by Bárbara Rodríguez 10. Cascading CSS Text Effects by Jhey","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"UI","slug":"UI","permalink":"https://kiko.io/tags/UI/"}]},{"title":"Discoveries #22 - Tips/Tricks","subtitle":null,"series":"Discoveries","date":"2023-01-06","updated":"2023-01-06","path":"post/Discoveries-22-Tips-Tricks/","permalink":"https://kiko.io/post/Discoveries-22-Tips-Tricks/","excerpt":"As someone said the other day? “January is the Monday of the year”. How true. After programming myself a new workflow for creating my discoveries (see Generate Content from Trello) at the end of last year, I wanted to try it out again right away and give you a list of tips and tricks for starting 2023. 6 steps to get verified on Mastodon with encrypted keysGenerate a Pull Request of Static Content With a Simple HTML FormMy Wonderful HTML Email Workflow, using MJML and MDX for responsive emailsHow to View Build Logs for GitHub PagesEnabling IntelliSense for GitHub Actions workflows in VS Code9 JavaScript Console Tips That Will Improve Your Debugging SkillsFun with console.log()Load hierarchical data from MSSQL with recursive common table expressionsAn HTML-first Mental ModelProject Documentation with Hexo Static Site Generator","keywords":"day january monday year true programming workflow creating discoveries generate content trello end wanted give list tips tricks starting steps verified mastodon encrypted keysgenerate pull request static simple html formmy wonderful email mjml mdx responsive emailshow view build logs github pagesenabling intellisense actions workflows code9 javascript console improve debugging skillsfun consolelogload hierarchical data mssql recursive common table expressionsan html-first mental modelproject documentation hexo site generator","text":"As someone said the other day? “January is the Monday of the year”. How true. After programming myself a new workflow for creating my discoveries (see Generate Content from Trello) at the end of last year, I wanted to try it out again right away and give you a list of tips and tricks for starting 2023. 6 steps to get verified on Mastodon with encrypted keysGenerate a Pull Request of Static Content With a Simple HTML FormMy Wonderful HTML Email Workflow, using MJML and MDX for responsive emailsHow to View Build Logs for GitHub PagesEnabling IntelliSense for GitHub Actions workflows in VS Code9 JavaScript Console Tips That Will Improve Your Debugging SkillsFun with console.log()Load hierarchical data from MSSQL with recursive common table expressionsAn HTML-first Mental ModelProject Documentation with Hexo Static Site Generator 6 steps to get verified on Mastodon with encrypted keys by Seth Kenlon https://opensource.com/article/22/12/verified-mastodon-pgp-keyoxide To verify that you control your Mastodon account, the easiest way is to add a verification link in your profile, which points to your blog/website and where Mastodon find a link attributed with 'rel=me'. For advanced verification you can use the power of shared encrypted keys, which Mastodon can link to thanks to the open source project Keyoxide … and Seth shows how to get it. Generate a Pull Request of Static Content With a Simple HTML Form by Hilman Ramadhan https://css-tricks.com/generate-a-pull-request-of-static-content-with-a-simple-html-form/ Hosting your static files blog/site/whatever on GitHub and wan't others to contribute? Hilman has an idea to achieve this via a standard form, that creates a Pull Request! My Wonderful HTML Email Workflow, using MJML and MDX for responsive emails by Josh Comeau https://www.joshwcomeau.com/react/wonderful-emails-with-mjml-and-mdx/ Writing HTML E-Mails can be challenging, because you can't use all the modern stuff. For a good reason the technique building mails has stuck in the 90s. Josh's tutorial is about using the framework MJML (Mailjet Markup Language), which offers an abstraction layer over raw HTML. How to View Build Logs for GitHub Pages by Rizèl Scarlett https://dev.to/github/visualize-github-pages-build-logs-1mc1 GitHub Pages are build with Jekyll and as the deployments runs with GitHub Actions it's easy to view the build details. But more interesting in Rizèl's article is as he describes a fully custom deployment without Jekyll. Enabling IntelliSense for GitHub Actions workflows in VS Code by Gérald Barré https://www.meziantou.net/enabling-intellisense-for-github-actions-workflows-in-vs-code.htm There are som VS Code Plugins out there, which supports Intellisense while writing workflow YAML files for configuring GitHub Actions. Gérald has a tip how to achieve that manually. 9 JavaScript Console Tips That Will Improve Your Debugging Skills by Sunil Sandhu https://blog.bitsrc.io/9-javascript-console-tips-that-will-improve-your-debugging-skills-1899e37469d5 The console is more powerful than you might think. Sunil talks here about the possibilities to debug a bit better and more efficient. I have to use 'time' more often… Fun with console.log() by Alicia Sykes https://dev.to/lissy93/fun-with-consolelog-3i59 In addition to Sunils tips above, Alicia summarizes it here and has some more tips for efficient debugging in the browser. Load hierarchical data from MSSQL with recursive common table expressions by Robert Muehsig https://blog.codeinside.eu/2019/03/31/load-hierarchical-data-from-mssql-with-recursive-common-table-expressions/ Designing a hierachie inside MS SQL can be painfull, but at least there is a way to load this data in a fast way, as Robert shows. An HTML-first Mental Model by Noam Rosenthal https://calendar.perfplanet.com/2022/an-html-first-mental-model/ Noam, from Google Chrome's speed metrics team, writes about his experiences on developing a showcase movies app using different frameworks regarding speed and performance in the browser and why we always keep good old HTML in mind. Project Documentation with Hexo Static Site Generator by Bruno Mota https://www.sitepoint.com/project-documentation-hexo/ Bruno Mota looks at how you can create project documentation using Hexo, the static site generator built on Node.js, and deploy easily to GitHub Pages. Some stuff to learn there for me, who runs this blog nearly the same way…","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Generate Content from Trello","subtitle":"Use Trello board as bookmark data source for generating Hexo content","series":"A New Blog","date":"2022-12-29","updated":"2022-12-29","path":"post/Generate-Content-from-Trello/","permalink":"https://kiko.io/post/Generate-Content-from-Trello/","excerpt":"I’m collecting/bookmarking links to interesting website post on a public Trello board and aggregating them from time to time in a special post series called Discoveries, where I present 10 of them in one post on a specific topic. Writing these summaries is currently still a bit time-consuming, because in addition to the link, the name of the author and a small description, I would also like to display a screenshot of the page in each case … and it is still a completely manual process. After selecting the 10 links I want to talk about, I first create a new post for my Hexo blog and then process the links as follows: Creating, resizing and saving the screenshot Creating a new section in the post Setting a key for the post based on the title Setting the title Setting the author Setting the screenshot file I do use two tag plugins (tag-anchorlist and tag-discovery) for this in the post draft, but despite that, it’s quite time-consuming and always the same procedure that can be wonderfully automated… and here I’d like to write about how I approached this task, while I’m working on it.","keywords":"im collecting˻ookmarking links interesting website post public trello board aggregating time special series called discoveries present specific topic writing summaries bit time-consuming addition link author small description display screenshot page case … completely manual process selecting talk create hexo blog creating resizing saving section setting key based title file tag plugins tag-anchorlist tag-discovery draft procedure wonderfully automated… id write approached task working","text":"I’m collecting/bookmarking links to interesting website post on a public Trello board and aggregating them from time to time in a special post series called Discoveries, where I present 10 of them in one post on a specific topic. Writing these summaries is currently still a bit time-consuming, because in addition to the link, the name of the author and a small description, I would also like to display a screenshot of the page in each case … and it is still a completely manual process. After selecting the 10 links I want to talk about, I first create a new post for my Hexo blog and then process the links as follows: Creating, resizing and saving the screenshot Creating a new section in the post Setting a key for the post based on the title Setting the title Setting the author Setting the screenshot file I do use two tag plugins (tag-anchorlist and tag-discovery) for this in the post draft, but despite that, it’s quite time-consuming and always the same procedure that can be wonderfully automated… and here I’d like to write about how I approached this task, while I’m working on it. The Trello ListHow easy it is to save a link as a card in a Trello board via Chrome, I described recently in my post Add website to Trello card the better way. As this works also in Chrome on Android, I store interesting links on the go mostly. In order to get all information I need later on, I have extended my collections board with a custom field called ‘Author’. For adding the screenshot to the card as an attachment, I use actually the build-in feature of Android 6. As I have a long, long list if incoming links, I sort them by topic into an appropriate list, for example, “Discoveries: JS Libraries” and this lists should then be automatically turned into new blog posts as I want to publish one. The DataNow, to get the raw data of a Trello list, I could use to the Atlassian API, but that’s not even necessary, because each board can be accessed machine-readable per se via adding .JSON to the board url. Prerequisite is that the board is set to PUBLIC. The URL is structured according to the following scheme: https://trello.com/b/<WORKPLACE-ID>/<BOARD-NAME>.json The URL accepts following parameters (as far as I found out), to filter out some not needed content: fields (string) lists (string) list_fields (string) cards (string) card_fields (string) card_attachments (bool) card_attachment_fields (string) customFields (bool) card_customFieldItems (bool) members (bool) member_fields (string) organization (bool) checklists (string) checklist_fields (string) labels (string) actions (string) action_fields (string) actions_limit (number) All boolean parameters can assume true or false and the string parameters either all, none or (some) a comma-separated value list of fields to show. For example: https://trello.com/b/o2tmzJAw/test.json?fields=none&lists=all&list_fields=name&cards=all&card_fields=desc,idList,name … shows up like this: Trello JSON (shortened)1234567891011121314151617181920212223{ "id": "63a871b59e3b200022455381", "cards": [ { ... }, { "id": "63a8586e7de45d0fc54d0d39", "desc": "Kate shows us how to create a tree view as collapsible list, created using only html and css, without the need for JavaScript", "idList": "63a8585dc0c10c020ca9ea03", "name": "Tree views in CSS" }, { ... } ], "lists": [ { "id": "63a997ab23684f02303a0525", "name": "Discoveries INBOX" }, { "id": "63a8585dc0c10c020ca9ea03", "name": "Discoveries: CSS" } ]} Important to now here is, that most of the data are NOT hierarchially structured, like Board > List > Cards, but in parallel. You have to pick the id of a list to filter the cards array by it. The same with custom fields inside a card: it holds a reference to the custom field list only. For the attachments of a card, Trello distinguishes between URL’s and files. The attribute bytes is null for URLs and the URL itself is in name. Files, on the other hand, have bytes greater than 0 and a specific mimeType, while images additionally have up to seven different previews in the widths 70, 150, 250, 300, 600 and original. Very handy for my case, since I always scale down my screenshots to 600 pixels. https://trello.com/b/o2tmzJAw/test.json?fields=none&lists=all&list_fields=name&cards=all&card_fields=desc,idList,name&card_attachments=true&card_attachment_fields=bytes,mimeType,name,previews&customFields=true&card_customFieldItems=true ... card item in Trello JSON (shortened)123456789101112131415161718192021222324252627282930313233{ "id": "63a8586e7de45d0fc54d0d39", "desc": "Kate shows us how to create a tree view as collapsible list, created using only html and css, without the need for JavaScript", "idList": "63a8585dc0c10c020ca9ea03", "name": "Tree views in CSS", "attachments": [ { "bytes": null, "mimeType": "", "name": "https://iamkate.com/code/tree-views/", "previews": [], "id": "63a8586e7de45d0fc54d0d5b" }, { "bytes": 225271, "mimeType": "image/png", "name": "Screenshot_20221225-121537.png", "previews": [ { ... }, { "_id": "63a8586e7de45d0fc54d0d58", "id": "63a8586e7de45d0fc54d0d58", "scaled": true, "url": "https://trello.com/1/cards/63a8586e7de45d0fc54d0d39/attachments/63a8586e7de45d0fc54d0d52/previews/63a8586e7de45d0fc54d0d58/download/Screenshot_20221225-121537.png", "bytes": 111574, "height": 499, "width": 600 }, { ... }, ], "id": "63a8586e7de45d0fc54d0d52" }} The GeneratorFirst of all: This generator has NOTHING to do with Hexo’s build-in generators. It’s just a Node script, which produces MD files that later on will be processed by Hexo into posts! What should he do: Download Board JSON from Trello Iterate through lists to find the one to process Iterate through cards to find all referencing the chosen list Create new POST object to store all needed information Process all found cards… Create new ITEM object to store all needed information Store TITLE, generated KEY out of TITLE, DESCRIPTION in ITEM Resolve AUTHOR from customfields for ITEM Iterate through card attachments Store URL in ITEM, when its a link Generate IMAGENAME out of KEY and store in ITEM, when its an image Create new POST.FOLDER for the images Download image from attachment URL into POST.FOLDER as IMAGENAME Add ITEM to POST.ITEMS Get photograph for new post (kiko.io special, see Automatic Header Images in Hexo) Store PHOTOGRAPH information in POST Generate new post via Handlebars template Store new post The goal is that I only need to write an introduction and adjust a few frontmatter variables before generating and publishing the post. SettingsFirst task is to save the possible settings in Hexo’s default configuration file: 123456789101112131415161718192021222324# Trello Discoveries Generator Scriptdiscoveries: board: url: https://trello.com/b/D6zIhLus/collections.json parameters: - key: fields value: name - key: lists value: all - key: list_fields value: name - key: cards value: all - key: card_fields value: closed,desc,idList,name - key: card_attachments value: true - key: card_attachment_fields value: bytes,mimeType,name,previews - key: customFields value: true - key: card_customFieldItems value: true template: discoveries.handlebars TemplateNext step is creating a Handlebars template out of my scaffold file I used so far: 123456789101112131415161718192021222324252627282930313233343536373839---slug: {{{key}}}title: '{{{title}}}'subtitle:date: {{{date}}}photograph: file: {{{photograph.file}}} name: {{{photograph.name}}} link: {{{photograph.link}}} socialmedia: /static/images/social-media/{{{key}}}.pngseries: Discoveriescategories: - Misctags: - Collectionrelated: - Discoveries-xx - Discoveries-yy - Discoveries-zzsyndication: - host: Mastodon url: ---INTRODUCTION...{% anchorlist {{#each items}} "{{{title}}}|{{{key}}}"{{/each}}%}<!-- more -->{{#each items}}{% discovery "{{{title}}}" "{{{author}}}" "{{{url}}}" {{{../key}}} {{{imageName}}} %}{{{description}}}{% enddiscovery %}{{/each}} Generator ScriptNext, the generator itself, which lives in /lib/scripts/. It is implemented as a class with CommonJS and takes two parameters in the constructor for defining the number of the Discoveries post to create and the name of the Trello list, where the data for this should come from. It’s main function is generate, which starts the generation. Here’s the skeleton: discoveries-generator.js123456789101112131415161718192021222324252627282930313233343536const fs = require("fs");const yaml = require('js-yaml');const path = require("path");const axios = require("axios");const handlebars = require("handlebars");const { marked } = require('marked');class Generator { let _config; let _trelloUrl; let _templateFile; // Init new post object let _post = { board: null, list: null }; constructor(discoveryNo, listName) { // Init the generator and gather all necessary data for running it } generate() { // Run the generator } async downloadImage(url, item) { // Helper for downloading the images asynchronously } createPostFromTemplate() { // Helper for creating the post's MD file out of the template }} At this point I will refrain from reproducing the complete code here. Just follow this link … However, a few points in the implementation are important to consider: The downloads are performed by means of the promise based HTTP client axios. I can only recommend this thing… The image downloads are initially collected in a Promise list for subsequent execution, while iterating through the cards of the selected Trello list The script is actually really straight forward, but I have some improvements in mind, which will find their way into the code later on: A. If an image is missing, create a proper screenshot via Puppeteer B. Introduce a top most card(s) for the INTRODUCTION, a SUBTITLE and some additional TAGS, to avoid having to rework the new post before publishing C. Automated insertion of the RELATED posts, based on the last three Discoveries issues Run it …The easiest way to get the generator running, is to create a simple runner script: _run_discoveries-generator.cjs1234567const Generator = require("./discoveries-generator.cjs").Generator;const discoveryNo = process.argv[2].toString();const listName = process.argv[3].toString();const generator = new Generator(discoveryNo, listName);generator.generate(); The execution in the console then is just a one-liner: node "./lib/_run_discoveries-generator.cjs" "<NUMBER>" "<TRELLO-LISTNAME>" It was fun to write this automation during Christmas. Also kept me from stuffing too many cookies inside me ;)","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Trello","slug":"Trello","permalink":"https://kiko.io/tags/Trello/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"}]},{"title":"Discoveries #21 - Sites & Pages","subtitle":null,"series":"Discoveries","date":"2022-12-28","updated":"2022-12-28","path":"post/Discoveries-21-Sites-Pages/","permalink":"https://kiko.io/post/Discoveries-21-Sites-Pages/","excerpt":"Websites always have a certain purpose and depending on what you want to do with it, there are pre-designed tools, that make it very easy to get started. In this issue of Discoveries I have collected a few such enablers. Also included are two hosting offers that address the hot topics of #Fediverse and #IndieWeb in times of #TwitterMigration. Webmention AnalyticsIndiekitMasto.hostStatic Timeline GeneratorGlossary Page TemplateMarkdocdocs.pageJPageGitHub OCTO: Flat DataBatNoter","keywords":"websites purpose depending pre-designed tools make easy started issue discoveries collected enablers included hosting offers address hot topics #fediverse #indieweb times #twittermigration webmention analyticsindiekitmastohoststatic timeline generatorglossary page templatemarkdocdocspagejpagegithub octo flat databatnoter","text":"Websites always have a certain purpose and depending on what you want to do with it, there are pre-designed tools, that make it very easy to get started. In this issue of Discoveries I have collected a few such enablers. Also included are two hosting offers that address the hot topics of #Fediverse and #IndieWeb in times of #TwitterMigration. Webmention AnalyticsIndiekitMasto.hostStatic Timeline GeneratorGlossary Page TemplateMarkdocdocs.pageJPageGitHub OCTO: Flat DataBatNoter Webmention Analytics by Max Böck https://mxb.dev/blog/webmention-analytics/ Your blog supports Webmentions? Then you should have a look on Max project, which I'm also contributing to. It collects all Webmention data of your post and gives you a nice analytics page. Indiekit by Paul Robert Lloyd https://getindiekit.com/ This Node-driven server is everything you need to start into the IndieWeb. Publish and share your own content, integrate SSG's like Hugo or Jekyll and file storage on GitHub, GitLab or FTP. It works with the Micropub protocol and has a plugin API for developing extensions. Masto.host by Hugo Gameiro https://masto.host/ Since a crazed billionaire has decided to make something 'different' out of Twitter, the Fediverse alternative Mastodon gains more and more attraction. #TwitterMigration. But it is not easy to host an instance by yourself. Hugo jumps in here and offers a fully managed Mastodon hosting service. Static Timeline Generator by Molly White https://github.com/molly/static-timeline-generator Some data are best presented by a timeline in order to show what has happened in descending order. Best example is actually twitterisgoinggreat.com, which lists all things happened since Elon Musk has taken over Twitter. This site is built with Mollys static timeline generator. Glossary Page Template by Hilverd Reker https://glossary.page/template/ Ever had to maintain a glossary? Can be very time consuming, first of all to present it in a structured, searchable way. This project from Hilverd, available on GitHub, is a single HTML page with a build-in editor. Markdoc by Stripe Dev's https://markdoc.io/ Markdoc is a Markdown-based system for creating custom documentation sites, build by the guys from Stripe on order to provide a documentation of their service to their customers. It is available at GitHub and a good starting point for your next documentation. You are documenting your stuff, right? docs.page by Invertase Dev's docs.page Another way to easily create documentation. This time with the focus on GitHub projects and from the developers of the British company Invertase It is as simple as the name suggests: create a /docs folder in the repository, put the configuration in a docs.json and start with the first MDX file… JPage by Pedro Isac https://pedro-isacss.github.io/jpage/ Some sites just need to have some slides to present the main purpose. Marketing stuff, photos, whatever. That's what Pedro built his JPage for. It supports slides in two axis and its configuration is just about the arrangement of standard HTML tags. Super simple and super effective. GitHub OCTO: Flat Data by Idan Gazit, Amelia Wattenberger, Matt Rothenberg, Irene Alvarado https://octo.github.com/projects/flat-data Flat is an experiment from the Developer Experience team in GitHub Next and gives you the possibility to fetch, aggregate and view data of many different types. It incorporates three different pieces: Flat Action to fetch data, Flat Editor and Flat Viewer. It's based on GitHub Actions and can be fully integrated in your repository. BatNoter by GitHub User vivekweb2013 https://github.com/batnoter/batnoter Batnoter is an open source, markdown-based, self-hosted note taking webapp, written with React, that uses your github repository to store markdown notes.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Hosting","slug":"Hosting","permalink":"https://kiko.io/tags/Hosting/"}]},{"title":"The State of the Blog","subtitle":"Recap & Workflow after 3 years with Hexo","series":"A New Blog","date":"2022-12-23","updated":"2022-12-23","path":"post/The-State-of-the-Blog/","permalink":"https://kiko.io/post/The-State-of-the-Blog/","excerpt":"I’ve started this blog in 2019 with this article primarily because I needed an area to record things learned for myself, with the side effect that others can benefit from it if they want. Why my choice fell on the static site generator Hexo, I no longer know, but I have now become accustomed (even to the shortcomings) and so far I have been able to implement all my ideas in it … and I had a few of them. In this post I would like to share a few experiences I have had with Hexo, regarding the main functionality and the things I’ve customized and describe my workflow behind the individual features of my blog. The latter is not as straightforward as I would like it to be, especially because I have different devices in use that require different approaches. The main purpose of this post is to simply write down for me, how things currently work on kiko.io and to have one or the other idea how to do it better while writing. Doing this publicly is in the hope that you might read this and have a terrific idea that I haven’t come up with yet and leave a comment or webmention … :)","keywords":"ive started blog article primarily needed area record things learned side effect benefit choice fell static site generator hexo longer accustomed shortcomings implement ideas … post share experiences main functionality customized describe workflow individual features straightforward devices require approaches purpose simply write work kikoio idea writing publicly hope read terrific havent leave comment webmention","text":"I’ve started this blog in 2019 with this article primarily because I needed an area to record things learned for myself, with the side effect that others can benefit from it if they want. Why my choice fell on the static site generator Hexo, I no longer know, but I have now become accustomed (even to the shortcomings) and so far I have been able to implement all my ideas in it … and I had a few of them. In this post I would like to share a few experiences I have had with Hexo, regarding the main functionality and the things I’ve customized and describe my workflow behind the individual features of my blog. The latter is not as straightforward as I would like it to be, especially because I have different devices in use that require different approaches. The main purpose of this post is to simply write down for me, how things currently work on kiko.io and to have one or the other idea how to do it better while writing. Doing this publicly is in the hope that you might read this and have a terrific idea that I haven’t come up with yet and leave a comment or webmention … :) Hexo …I don’t have experience with other SSGs like Hugo or others, so I can’t make comparisons, but I can share a few things about Hexo that have kept me engaged in my work with Hexo. I won’t go into the general functionality of Hexo here, but the ones that interest me most for my particular needs. A more or less good start into the tool is https://hexo.io with its DOCS and API. One of the first commands you get to know is generate, which generates the static HTML pages from the MD files stored in the source/_posts folder. You can add your own generators, which can generate any other custom pages, to this process and put them under themes/<your-theme/scripts for automatic execution. Every generator has a locals argument, which holds all site variables, including a list of all posts. Unfortunately, the documentation at hexo.io/api is rather poor and so only after a while I realized, that a custom generator is called relatively late in the process, when the MD files have long since been preprocessed. So you can process any MD files in a generator, but you have to do without the automatic processing of the powerful Tag Plugins or syntax highlighting, because this takes place in an upstream processor, which deals with boxes. I don’t understand Hexo’s concept at this point. For me it is not particularly catchy and straightforward. Unfortunately a few help-, discussion- and example pages are written in Chinese, since the creator Tommy Chen is from Taiwan, as I guess, but my Chinese really sucks… ;) The best thing about Hexo in general is, that you can get your first results quickly after the basic installation, so you are able to concentrate on writing and prettying things up for a while. Only when the requirements increase is it necessary to take a closer look at the insufficiently documented substructure. A challenge even for experienced web developers, but unfortunately absolutely impossible for beginners… PostsHexo is based on a structure of posts and pages, where posts can be aggregated in various ways, whether in the time-based archive or via tags and categories. Posts has to have a specific structure, regarding the Frontmatter in the MD file and teh storage of attached images. Hexo offers the user a separate command for this: hexo new, which works with template files in the folder scaffolds. For the presentation of my posts, I decided to incorporate my passion of photography by assigning a unique header image to each post (and most pages). How I implemented this, I have described in the article Automatic Header Images in Hexo. The main point is, when I create a new post, I automatically and randomly pick a photo (in the three media-dependent versions normal, tablet and mobile) from a pool, copy the images to the appropriate delivery folder and bind it to the new post via frontmatter. For this I’ve written the Hexo event hook on-new-get-photo-for-post.js, which runs on the command hexo new post "<title-of-new-post>". Works great, except for the fact that I’m often on the go and using a Node.JS-powered command towards an SSG project hosted on GitHub definitely doesn’t work on an Android smartphone. So, how do I get it to create a new post remotely? First part of the answer, regarding access to all project files, was to store the project in a folder that is synchronized with one of the usual suspects: Dropbox, OneDrive, GoogleDrive. With this, I have access to the entire project at all times on all devices that can sync the files. In the case of Android with the help of one of the Autosync apps from metactrl. The second part of the answer, regarding execution of the new post command, was a bit more difficult, but part of my solution was to use my little home server, where the project also syncs and which is always switched on. Problem here: This tiny box is not publicly available on the web and never will be. Therefore the only communication channel I had, was file synchronization and the solution was to use a simple text file to hold the commands I want to be executed. For this purpose, I have written a small command line tool that runs and processes a modified command file when it is received. For file monitoring I have SyncBackPro in use, which has the incredibly useful feature of being able to execute commands before and after a synchronization takes place, in this case my tool, which I have named HexoCommander and whose source code is available on GitHub. Before you think “What the hell is he doing? That’s what [fill in the Linux tool of your choice] is for“ … I’m more of a Windows guy. Linux means something to me, but I’ve never worked with it and never had to … and yessss, there is a career in IT also with Windows ;) The advantage of synchronizing the whole GitHub project via cloud storage service is that I also host kiko.io on Github Pages and that a commit to the repository on GitHub automatically triggers a deployment, which I don’t always want right away. So, because of my changes to the post process, I don’t use Hexo’s built-in drafts, but write on a new post until I’m done and then release by committing to GitHub. is there an easier way? I admit that on an Android smartphone, it’s a bit fiddly to write a special command to a text file and then wait 2 or 3 minutes for the file to sync, the new line to be read and executed, and the new post files to sync back to the smartphone, but it works. But I would rather have a smartphone capable interface, where I can enter my data and the whole thing runs at the push of a button. Currently I have no idea how to achieve this in a static site environment. Do you have one? PhotosAs I speak from photos … to provide header images for my new posts (or pages), I have prepared a pool of photos that I display on a special filterable page. This page is not one that comes with Hexo, but is based on a dynamic page generator I’ve created for this purpose. I wrote about it in 2021: Pattern for dynamic Hexo pages. But how do I fill this pool with new photos, especially because I need three variants of a new photo for the standard device classes Desktop (normal), Tablet and Mobile? I have a very specific workflow in Lightroom to edit my photos and convert them from RAW to JPEG while exporting, to add them to my 500px collection for example. The destination of these export is again my cloud storage. From time to time I pick some images from these export folders that I want to use on the blog. I do this mostly on my Android smartphone in a quiet minute and these are the steps and Android apps I need, to turn a high resolution 10MB photo into one that can be used on the web: Solid Explorer: Creating a new folder in the projects pool folder with the name of the image. Solid Explorer: Copy the original photo into the new pool folder. Solid Explorer: Create meta.txt to hold the name and the 500px Url of the photo in the new pool folder. ImageSize: Create normal (1280px), tablet (768px) and mobile (480px) versions of the photo as web images. Solid Explorer: Copy web images to new pool folder and delete original. I have a similar workflow working on my Windows machine, but both workflows are manual ones. is there an easier way? Fiddling around with images on the smartphone like this almost has a medidative effect on me, but it’s not really effective. Better would be to throw the original image into a folder and let Hexo do the work of resizing by utilizing Sharp and imagemin. Series & ProjectsRelatively soon it was clear to me, that I want to aggregate posts to series and I didn’t want to use the integrated categories or tags for it. It should be working with a new frontmatter attribute called series with the value of a slug of a referencing MD file, which provides some text to describe the series. Projects is basically a index like series, but merely works with a different Frontmatter attribute. The whole thing was made possible by Levi Wheatcroft‘s work on his plugin hexo-index-anything. Unfortunately Levi stopped working on it a long time ago and so I forked it under the new name hexo-generator-anything. The generator is controlled by a section in Hexo’s configuration file, which defines the EJS layout file to render the main index page respectively the index pages of each series, beside a mapping list for the appropriate Frontmatter variable and the output path: _config.yaml12345678anything: layout_index: anything-index layout_posts: anything-posts index_mappings: - variable: series path: series - variable: project path: projects Tag Plugins and syntax highlighting for code blocks in the reference MD files are not resolved actually, because it is a generator only (see first section ‘Hexo…’). Tiny-ToolsSince this blog is primarily a memory aid for me and my future self (how often do I have stumbled across my own posts during web search), it was logical to place a special bookmark collection here, because it happened to me more often that I found and used an online tool and some time later wonder what the thing was called when I have the same requirement again. That’s why I started to collect these URL’s in a public Trello board (see Add website to Trello card the better way) and provide them with keywords and a screenshot. But Trello is a Kanban tool and not designed for such special requirements as the display of bookmarks and so I use it only as a data source for my page TINY-TOOLS. Generating a static page out of Trello cards was again a job for my dynamic pages, I had introduced for the photos page. It takes advantage of the fact that any Trello board can be retrieved as machine-readable data by extending the url with .json. The Dynamic Trello Generator is really straight forward. With this script it is only a matter of configuration to generate a page from a given list of a board (URL ist shortended): 1234567trello: boards: - name: Collections url: https://trello.com/.../collections.json?fields=all&cards=all&card_fields=all&card_attachments=true&lists=all&list_fields=all pages: - name: tiny-tools list: TinyTools is there an easier way? Can’t imagine one. Do You? NotesThe Notes section is the latest and with it I want to follow the POSSE principle, i.e. “Publish (on your) Own Site, Syndicate Elsewhere”, which is a vital part of the IndieWeb movement. So when something comes to mind that I want to post on Mastodon for example, I want it to end up in a new Note MD file that is then posted, preferably automatically. For me, notes differ from normal posts. From the beginning I wanted to present them chronologically connected on one page per year, like a diary, and not to mix them with the articles (posts). So a standard treatment as a Hexo post was out of the question and I had to come up with something of my own. Another reason not to work with the standard structures of Hexo was, that I did not want to create a subfolder with the name of the post for images as usual in Hexo, but one folder for all images of a year. Since I am familiar with Hexo generators, I built a new one called generator-notes that iterates through the entire source/_notes folder and generates an index file for all the notes in a year, as well as the notes file itself, and copies the images to the source path. This approach also freed me from using my HexoCommander, because there is no need of executing a new xxx command. I can now create new Notes from the smartphone quite easily: Autosync App: Synchronization of the source/notes folder Markor: Edit or create new Note MD file via snippet. GitHub PWA: Upload MD file to the appropriate folder of the repository The upload will automatically trigger the build and deployment to GitHub. Done. is there an easier way? Using a generator directly means, that I can’t use tag plugins in my Notes and code blocks won’t be prettied up, as I mentioned earlier. Also the Notes are not included in the Hexo Locals and are therefore not available later on for RSS feeds or other stuff to create. So I will probably extend my approach with a custom Hexo processor, that will allow me to do this. DeploymentAs I mentioned earlier, kiko.io is hosted at GitHub Pages. Due to a bug in Hexo regarding the treatment of non markdown files in post folders (Fix #1490), I had to process all files locally and transfer the output folder to the repository as well. I would have liked to use a GitHub action for the entire deployment, including the generation of the output files, but for a long time there was no NPM package with a working Hexo version that would have allowed this. I recently checked again to see if the current Hexo release 6.0.3. had fixed my bug and the subsequent bugs …. and yes. With one or two workarounds, my Hexo installation now runs completely on NPM packages, without any manual adjustments!So I could finally switch the deployment and automate it with its own build action. This enables me to write a text on the go, commit the file via github.com and the build and deployment starts. Woohoo … \\o/ is there an easier way? Nope. The only drawback I see is that this approach always generates the entire site and not just the newly added posts, as is the case locally. It’s a bit of a waste of time and computing resources. SyndicationAs I’m a fan of the Indieweb, I started early with Webmentions on my blog. The idea behind it is to mention an article from blog A in a post on Blog B or social media like Twitter or Mastodon and blog A gets an automated message like “Hey, I’ve mentioned you here“. The operator of Blog A can now also automatically display this message under the article mentioned. Receiving Webmentionskiko.io is a static site, therefore, there are no active components on the web server that could react to an incoming webmention. But that’s not a problem, because there are services like webmention.io that can serve as recipients and from where you can then pick up the messages. Some bloggers do this by fetching and inserting them into the articles while generating the static pages, but for me this can take too long. My approach is, to load the mentions from webmention.io via JavaScript dynamically into the page, as described in my post Hexo and the IndieWeb (Receiving Webmentions). is there an easier way? I don’t think so… Sending WebmentionsUnfortunately a I can’t integrate sending Webmentions for the mentioned Url’s in a particular (new) post into the generation process while building the static files for the blog, because the target Url’s not yet exist at this point of time. I have to wait until it is deployed and thus publicly available. So this step is currently still a manual one. My current solution is based on Remy Sharp’s NPM package @remy/webmention, which he himself also uses for his web service webmention.app. I integrate it into a Hexo console command called “console-webmention” which I call via hexo webmention. Without parameters, the last post is searched for outgoing url’s and their webmention endpoints respectively. See the project page for more details. is there an easier way? This works so far, but depending on a console application means once again having to use my HexoCommander and write commands to a text file when I’m on the road. Then I can also send the webmentions for a post directly manually via the Telegraph service. One possibility would be a cron job in a Github Action, but this would require creating a new container with all dependencies every time the build has just run and Github has deployed the created files, just to run a console command that might not even return any results since there is nothing to mention in the last post. Unsatisfactory. Another possibility would be to utilize my Atom feed by setting up a Zapier, IFTT or Make/Integromat task, which picks the last added entry and run the Telegraph API against it. Any ideas? Publish on MastodonI’m relatively new to Mastodon (Nov 22), but in the past I have syndicated some of my posts manually on Twitter. Now, that I’ve said goodbye to the greedy bird site, I’m thinking about how I’m going to automate publishing the notes in particular, but also at least an excerpt from a post, on Mastodon. What I do in the meantime? After the site is successfully deployed, I open up my Mastodon client and copy the complete content of a note or write an excerpt of a post manually into a new toot and append the permalink. After hitting SEND, I copy the newly created Mastodon post URL into a Frontmatter attribute called syndication of the note or post and commit the file to GitHub. Build and deployment runs again and the syndication link will be visible on kiko.io. is there an easier way? The Mastodon API is really simple and there are many examples written in Python, JS and other languages out there that are easy to adapt. But here I have the same problem as when sending webmentions: the target url is not yet available at the time of generating the page, when some code is running to do something. It must be a downstream task as well. I need some kind of service running on a server, where I only have a static site. Independent GitHub Action or Azure Function, triggered by a webhook after deployment is finished, which processes all notes and posts without the syndication Frontmatter? Or extending HexoCommander again, but how to trigger this buddy automatically, as he depends on file synchronization? SummaryHexo is a good SSG system with which you can achieve good results in a short time and which leaves enough room for your own creative ideas. The documentation needs a lot of improvement, especially regarding the details in depth and enriched with examples. I’m not surprised that Hexo seems to lag behind other systems like Hugo or 11ty. Maybe this also have something to do with communication. It’s not as much of a conversation in the Western space as others. In the future, I have planed to be more involved in matters of documentation and communication, even though I have not yet understood the full complexity of the substructure. However, I enjoy working with Hexo despite or because of the shortcomings. Coding without a challenge is like driving on a highway … boring :) My workflows are currently not really straightforward, especially with regard to connectivity. However, this is also due to the fact that there are no reactive components working on “my” web server. It’s all still way too much tinkering, but it will be fun over the next few months to edit all the remarks in the “IS THERE AN EASIER WAY?” blocks above and continue to develop the blog.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"Mastodon simply explained","subtitle":"Why it is not Twitter and never will be","date":"2022-11-15","updated":"2022-11-15","path":"post/Mastodon-simply-explained/","permalink":"https://kiko.io/post/Mastodon-simply-explained/","excerpt":"This weekend I had a revival experience. I got myself a Mastodon account, after many years of staying away from social media as far as I could and at most tweeting a new blog post. Since my active Twitter days, things have taken such a turn for the worse, that I’ve preferred to stay away completely. I don’t know what made me join Mastodon on Sunday, but it sparked something in me: The love for the good old internet of the late 90s, early 2000s. These guys gave me a warm welcome and made me feel that Internet communities, away from sleazy influencers and banner ads, could actually still work. I got a taste of it, when I let this blog become part of IndieWeb over a year ago, but didn’t quite believe in it because of the low traffic and mentions. Now the richest man on the planet has to come along and buy the bird site, and a niche product called Mastodon, well known but underestimated in tech circles, almost explodes under the onslaught of disappointed commercial users. And that’s what we are on Twitter, Facebook and Co … a commercial object. The problem now will be how the community can integrate these many users for whom it was not mentally prepared. What now adapts to whom? The most important thing, however, will be to teach the newcomers (and I count myself among them) that Mastodon is not Twitter in new clothes! I was going to write a few things about this, but this morning I read a Twitter thread from Peter Jakobs, a self-proclaimed digital Aboriginal from Germany, that just sums it all up and explains it so well I could hardly do it: That’s why I’m now doing something I’ve never done before: I quote Peter here completely, translated from German into English … Curtain up (and thank you DeepL :D):","keywords":"weekend revival experience mastodon account years staying social media tweeting blog post active twitter days things turn worse ive preferred stay completely dont made join sunday sparked love good internet late 90s early 2000s guys gave warm feel communities sleazy influencers banner ads work taste part indieweb year ago didnt low traffic mentions richest man planet buy bird site niche product called underestimated tech circles explodes onslaught disappointed commercial users facebook … object problem community integrate mentally prepared adapts important thing teach newcomers count clothes write morning read thread peter jakobs self-proclaimed digital aboriginal germany sums explains im quote translated german english curtain deepl","text":"This weekend I had a revival experience. I got myself a Mastodon account, after many years of staying away from social media as far as I could and at most tweeting a new blog post. Since my active Twitter days, things have taken such a turn for the worse, that I’ve preferred to stay away completely. I don’t know what made me join Mastodon on Sunday, but it sparked something in me: The love for the good old internet of the late 90s, early 2000s. These guys gave me a warm welcome and made me feel that Internet communities, away from sleazy influencers and banner ads, could actually still work. I got a taste of it, when I let this blog become part of IndieWeb over a year ago, but didn’t quite believe in it because of the low traffic and mentions. Now the richest man on the planet has to come along and buy the bird site, and a niche product called Mastodon, well known but underestimated in tech circles, almost explodes under the onslaught of disappointed commercial users. And that’s what we are on Twitter, Facebook and Co … a commercial object. The problem now will be how the community can integrate these many users for whom it was not mentally prepared. What now adapts to whom? The most important thing, however, will be to teach the newcomers (and I count myself among them) that Mastodon is not Twitter in new clothes! I was going to write a few things about this, but this morning I read a Twitter thread from Peter Jakobs, a self-proclaimed digital Aboriginal from Germany, that just sums it all up and explains it so well I could hardly do it: That’s why I’m now doing something I’ve never done before: I quote Peter here completely, translated from German into English … Curtain up (and thank you DeepL :D): Keyword “Mastodon for Twitterer” 🧵 I’ve now read from a few of you that Mastodon is too complicated, you’ve had problems, or you somehow don’t like it.As a stone age Twitterer (I created this account on Feb 13, 2007), I have by now half of my activities to Mastodon. Many of my posts you see here now come from there and I mostly come here to respond to your replies. So it is possible for an old white guy like me to move, and maybe I can ease some of your fears and help with the move. You ask why you should and that almost nothing has changed here (yet)? Maybe, but the signs on the wall don’t bode well. Elon Musk is, if you look at it neutrally, someone who destroys things in order to rebuild them - better, in his sense.But that means change is coming. It will disturb you here, but it will come here maybe not in one fell swoop like a move to Mastodon. I’m always a friend of bringing about change myself, because then I can shape it in my own way and am not dependent on others. But it could be that you will still like Twitter in 12 or 18 months. I know this myself, my Twitter experience is by no means as negative as many others say.But many others, like me, go a different way, and are thus no longer accessible here. But that is exactly one of the biggest disadvantages of Twitter and one of the advantages of Mastodon.Let me explain. Many write that they are confused by these “instances” (some say “groups” which is not quite true) on Mastodon.I can understand that, it can be confusing. But what if I told you that there is also an instance on Twitter, but just exactly one. Twitter.com namely. This one can’t communicate with anything or anyone else, an island in the middle of a vast social media ocean. And now this island has been stupidly bought by someone whose idea of what such an island should look like is rather special. Mastodon has many such islands and they are connected with bridges. You choose an island, pitch your tents there and - you are not tied to this island. On the one hand you can communicate completely freely with all inhabitants of all other islands, on the other hand you can, if you want to, take down your tents again and move to another island (you will lose a little bit, but not as much as if you want to leave here [at Twitter]). These islands (instances) are of different sizes. The biggest ones in Germany are mastodon.social, troet.cafe, mastodon.green and a few others. But there are also tiny islands, I know more than one with a single inhabitant. Is he isolated on his island? No, because he is connected with all the others via said bridges. Every island has one (or more) chiefs who keep order (Twitter does too, but we’ve complained about that a lot). These chiefs are often private individuals who run an instance as a hobby, but with the large instances there is usually a team behind it, and some also a small company (mastodon.social, for example, is run by a limited liability company that was founded by the chief developer of the Mastodon software, mastodon.green is, as far as I know, a one-man limited liability company). So now we know that there are many islands, that we are not trapped on these islands and that we can move.These islands are one of the standout features of Mastodon: a Musk, a Trump, or even a certain four-letter newspaper can buy an island or even build an island themselves, but they can’t possibly buy all the islands, simply because it’s so cheap to build new islands. (I read somewhere once, to run an instance costs about 1€ per user per month). But because the interesting thing about Twitter, like Mastodon, is not the medium itself, but the networks we build on it, it doesn’t matter what happens to individual islands as long as we can move freely. If individual islands do not want to comply with the rules at all, then they are “defederated”, or figuratively: the bridges there are demolished. However, this is the decision of each individual island chief, so it may not be grassroots democracy, but it is distributed. Ok pjakobs, enough of the chatter, how am I supposed to find out which island is suitable for me?Here is a searchable list of Mastodon instances: instances.social/list It is possible that another aspect of the “island” image will apply to you: there is a local timeline on the islands, which is also called that. So, if you choose an island that is particularly suited to a geographical region, a topic or a language, then you will see the contributions of the other “inhabitants of this island” in their local timeline - which hopefully have a certain thematic cohesion. However, as already mentioned, you can follow all people on all instances and you will also see them in your own timeline. Speaking of timelines: they are chronological and you only see the posts (toots) of those you follow. No algorithm, no “others like this too”, no good morning tweets in the late evening. Just the way Twitter used to be 10 years ago. Follow: here is one of the points that bothers me the most, even if it is a small thing. Currently there are two software versions of Mastodon in use: 3.5.3 and four, which seem to handle following a bit differently. In one (I assume the older version), if you want to follow an account on another instance, you have to specify your own handle again (in my case “@pjakobs@mastodon.green“ - but that’s no longer the case in the new version, there you click a follow button and you follow the account. Another thing that is very very different is DMs. Here on Twitter, we’re used to DMs essentially working like little emails or a messenger: we have our own inbox and that’s where the dialogue with the other person shows up. Feels good, not visible to others, because yes, in a completely different realm.In Mastodon, direct messages are simply toots that are displayed only to the recipient(s) - and normally among all the other interactions. It’s confusing and doesn’t feel as nicely protected as it does here, and as I walked around town a bit yesterday I realized: this is intentional. Twitter creates an apparent privacy through display that DMs don’t offer.DMs are deliberately not PM, not private messages. Mastodon also makes that clear through its presentation. I have therefore added my Threema and Telegram link to my profile, these tools actually offer private conversations. Is not comfortable but honest. You have created an account, but do not want to move 100% yet or, as in my case, still have many friends on this site? There is also a kind of “bridge” from Mastodon archipelago to Twitter. You can mirror your tweets to Mastodon or vice versa. This is only valid for tweets, not for replies. That means you have to be a bit careful that you don’t just spam your followers in the other network, but also interact with them. After a few days of mirroring from Twitter to Mastodon I turned the bridge around, today probably 80% of the tweets you see from me and the non-replies are on Mastodon. I use crossposter.masto.donte.com.br for this. The lovely people from @digitalcourage not only run a Mastodon instance themselves, but also put together a great overview of the Fediverse (the federated social media universe, which is much more than just Mastodon). In summary, if Musk is really fucking this up, you don’t have to be a helpless victim of his actions. Even if Mastodon might not be an alternative for you at the moment, create an account, try to find your followers over there. A life raft is not as comfortable as a floating boat, but it’s better than a sinking boat! Ah, here’s another more complete overview of Mastodon:the-federation.info/mastodon (takes a little while to load) --- Peter Jakobs You will find Peter Jakobs on this island: @pjakobs@mastodon.green. My new social media residencies are: @kiko@indieweb.social for Tech Stuff in English and @kiko@hessen.social for politics and society in German. Happy Tooting !","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Social Media","slug":"Social-Media","permalink":"https://kiko.io/tags/Social-Media/"},{"name":"Mastodon","slug":"Mastodon","permalink":"https://kiko.io/tags/Mastodon/"}]},{"title":"Syndicate Mastodon Hashtags in your favorite Feed Reader","subtitle":null,"date":"2022-11-13","updated":"2022-11-13","path":"post/Syndicate-Mastodon-Hashtags-in-your-favorite-Feed-Reader/","permalink":"https://kiko.io/post/Syndicate-Mastodon-Hashtags-in-your-favorite-Feed-Reader/","excerpt":"Ok, I admit it: I read RSS feeds. Quite old school you might think, but I’m mostly off Social Media and the most news sites quite a while ago, with a few exceptions. I just want to read selective stuff, especially in the direction of technology, and not interrupted by items, the news provider think I have to read. My favorite tool for my feed collection is Feedly, which I open up almost every morning. Today, Max Böck gave me the momentum with his article The IndieWeb for Everyone to try another type of social media I know for quite a long time, but never give it a chance: Mastodon. I’m now part of it on indieweb.social.","keywords":"admit read rss feeds school im social media news sites ago exceptions selective stuff direction technology interrupted items provider favorite tool feed collection feedly open morning today max böck gave momentum article indieweb type long time give chance mastodon part indiewebsocial","text":"Ok, I admit it: I read RSS feeds. Quite old school you might think, but I’m mostly off Social Media and the most news sites quite a while ago, with a few exceptions. I just want to read selective stuff, especially in the direction of technology, and not interrupted by items, the news provider think I have to read. My favorite tool for my feed collection is Feedly, which I open up almost every morning. Today, Max Böck gave me the momentum with his article The IndieWeb for Everyone to try another type of social media I know for quite a long time, but never give it a chance: Mastodon. I’m now part of it on indieweb.social. And as you just do … you surf around a bit on it and read a few things until a post by Matthias Ott caught my attention: Ok, nice … but does this work for other Mastadon things also, like hashtags? I have to say that I am currently very interested in creating PWAs and the techniques behind them, and I’m always looking for new resources to read. So it didn’t take me 5 minutes to start reading posts in Mastodon with the hashtag #pwa almost automatically. What can I say? It works… Step 1Search for an hash tag, f.e. #pwa. It will lead to an Url like this, depending on your Mastodon server: https://indieweb.social/web/tags/pwa Step 2Cut out /web Step 3Add .rss … and you got your feed on a special topic … … you can add to your favorite feed reader. Happy Reading…","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Mastodon","slug":"Mastodon","permalink":"https://kiko.io/tags/Mastodon/"}]},{"title":"Anatomy of Service Worker Communication","subtitle":"Let your App communicate with its Service Worker","date":"2022-11-12","updated":"2022-11-12","path":"post/Anatomy-of-Service-Worker-Communication/","permalink":"https://kiko.io/post/Anatomy-of-Service-Worker-Communication/","excerpt":"I have a SPA that works as a PWA, which means that in the background a service worker makes sure that the required files for the offline mode end up in the cache. From time to time I also update the Service Worker, which defines which files it should keep offline and which not. Unfortunately, the app itself didn’t get any of this because there was no communication channel for them to talk. If you research this topic on the web, you have to dig through many architecture pages and documentations that have one thing in common: sometimes they just don’t get to the point. So here are my 50 cents on the subject and my sample implementation.","keywords":"spa works pwa means background service worker makes required files offline mode end cache time update defines app didnt communication channel talk research topic web dig architecture pages documentations thing common dont point cents subject sample implementation","text":"I have a SPA that works as a PWA, which means that in the background a service worker makes sure that the required files for the offline mode end up in the cache. From time to time I also update the Service Worker, which defines which files it should keep offline and which not. Unfortunately, the app itself didn’t get any of this because there was no communication channel for them to talk. If you research this topic on the web, you have to dig through many architecture pages and documentations that have one thing in common: sometimes they just don’t get to the point. So here are my 50 cents on the subject and my sample implementation. Preliminary ThoughtsIn most examples I’ve read, the authors talk about code for the app itself and code for the Service Worker, but that falls short to me, because in my opinion there are THREE parts: The App The Service Worker The Service Worker Management, which takes care of the proper registration and installation of the service worker The last part of the code shouldn’t be part of the app itself, in the meaning of SoC (Seperation of Concerns). It does not contribute to the functioning of the app. The AppHere’s the general anatomy of my App: app.js123456789101112131415161718192021var app = { 'settings': { // some stuff on app settings ... }, 'pages': { // views of the SPA ... }, 'ui': { // some UI helper 'dialog': function(msg, title) { // show the message in a toast, popup or elsewhere } }, 'starter': { 'init': { // initializing of the app ... } }}window.app = app;app.starter.init(); Nothing unlikely, as I guess. It is built according to the composite pattern and has one entry point, that is called at the end of the file after it is instantiated. The Service WorkerThe Service Worker lives in its own JS file and its implementation is really straight forward: service-worker.js12345678910111213141516171819202122232425262728var cacheName = 'my-apps-cache-v1.2.3';var appFiles = [ // list of app relating files, that has to be handled]var excludeUrls = [ // list if URL's that shouldn't be handled]self.addEventListener('install', function(e) { // on install, open the cache and add all appFiles ... //activate immediatly and dont wait for connected clients to disconnect self.skipWaiting();}self.addEventListener('activate', function(e) { // remove old caches //say to all clients: Now I'm responsible self.clients.claim();}self.addEventListener('fetch', function(e) { // intercept requests and serve the app files out of the cache} I won’t go into the depth of my implementation here now, since it doesn’t matter for the message exchange. It is only good for you to know that I have versioned the cache name to exchange it with new versions. The Service Worker ManagementNow the management code for the Service Worker. It has to be loaded with the app code, because it is client code and later on it needs knowledge of the app. sw-management.js1234567891011121314151617181920212223242526272829303132if('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js') .then(function(registration) { // detect Service Worker update available registration.addEventListener('updatefound', function() { if (registration.installing) { // detect install of new Service Worker registration.installing.addEventListener('statechange', function() { if (registration.waiting) { if (navigator.serviceWorker.controller) { // there is an existing controller that has been updated //TODO: Send a message to the app } else { // first install } } } } } }} Lets Communicate…In this setup, the communication code can be implemented. Let’s do it as a round trip. 1. Client sends message to Service WorkerAs sw-management.js represents our Service Worker management code, we add a little function to send a message in here: sw-management.js1234567function sendMessageToServiceWorker(type, msg) { if (navigator.serviceWorker.controller) { navigator.serviceWorker.controller.postMessage( {'type': type, 'msg': msg } ); }} We define a type for the purpose of our communcation and a message itself. The function can be called wherever, like this: 1sendMessageToServiceWorker("TEST", "Hey, Service Worker"); 2. Service Worker receives the messageIn the Service Worker code we need to add a recipient: service-worker.js1234567self.addEventListener('message', function(event) { if (!event.data) return; if (event.data.type === 'TEST') { // do something regarding to the type and/or with the message }} 3. … and sends a message back to the clientWhat we want to do with the message depends what we want to achive, but in this example, just let’s greet back: service-worker.js12345678910111213141516171819function sendMessageToClients(type, msg) { // as the SW can control multiple clients, we have to catch them all self.clients.matchAll({ includeUncontrolled: true }) .then(function(clients) { for (const client of clients) { client.postMessage( {'type': type, 'msg': msg } ); } });}self.addEventListener('message', function(event) { if (!event.data) return; if (event.data.type === 'TEST') { sendMessageToClients(event.data.type, 'Hi Clients...'); }} 4. Client receives the answer from the Service WorkerIn order to get messages from the Service Worker, we have to implement a receiver in the client also: sw-management.js12345678910if('serviceWorker' in navigator) { //... 'navigator.serviceWorker.register' stuff navigator.serviceWorker.addEventListener('message', function(event) { if (event.data) { // do something with the message from the Service Worker } });} 5. … and shows it in the appAs I pointed out earlier, that the management code has to be loaded alongside with the app code, it’s a breeze to show the message: app.js1234567891011121314var app = { ... 'ui': { // some UI helper 'dialog': function(msg, title) { // show the message in a toast, popup or elsewhere } } ...}window.app = app;... sw-management.js12345678910if('serviceWorker' in navigator) { //... 'navigator.serviceWorker.register' stuff navigator.serviceWorker.addEventListener('message', function(event) { if (event.data) { app.ui.dialog.info(event.data.msg, 'Service Worker says...'); } });} The Update MessageWith this infrastructure, everything is there to show a message, when the Service Worker is updated: sw-management.js123456789101112131415161718192021222324252627282930313233343536if('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js') .then(function(registration) { // detect Service Worker update available registration.addEventListener('updatefound', function() { if (registration.installing) { // detect install of new Service Worker registration.installing.addEventListener('statechange', function() { if (registration.waiting) { if (navigator.serviceWorker.controller) { // there is an existing controller that has been updated app.ui.dialog.info('New version installed', 'Service Worker'); } else { // first install } } } } } } ...} Important to point out, that the Service Worker side of the communication is not involved in this case, because only the client-side management code knows when a new version has to be installed. More Info Jake Archibald: The service worker lifecycleDemian Renzulli & Andrew Guan: Two-way communication with service workersAdam Bar: Handling Service Worker updates – how to keep the app updated and stay saneFelix Gerschau: Service Worker Lifecycle ExplainedFelix Gerschau: How to communicate with Service WorkersPeter Kröner: PostMessage zwischen Service Worker und Client(s) (GERMAN)","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"SPA","slug":"SPA","permalink":"https://kiko.io/tags/SPA/"},{"name":"PWA","slug":"PWA","permalink":"https://kiko.io/tags/PWA/"}]},{"title":"Discoveries #20 - CSS & UI","subtitle":null,"series":"Discoveries","date":"2022-10-08","updated":"2022-10-08","path":"post/Discoveries-20-CSS-UI/","permalink":"https://kiko.io/post/Discoveries-20-CSS-UI/","excerpt":"Web interfaces are unthinkable without CSS. It has its pitfalls, but when used correctly it’s damn powerful. It’s always incredible what developers do with it. This month’s Discoveries is about the basics and the amazing. My Custom CSS ResetDefensive CSS10 Useful CSS Tricks for Front-end DevelopersAnimated Star RatingCSS Marquee ExamplesCSS Rolling TextCool Hover Effects That Use CSS Text ShadowSolving 'The Dangler' Conundrum with Container Queries and :has()Conditionally Styling Selected Elements in a Grid ContainerIntersection Observer Scrolling Effects","keywords":"web interfaces unthinkable css pitfalls correctly damn powerful incredible developers months discoveries basics amazing custom resetdefensive css10 tricks front-end developersanimated star ratingcss marquee examplescss rolling textcool hover effects text shadowsolving dangler conundrum container queries hasconditionally styling selected elements grid containerintersection observer scrolling","text":"Web interfaces are unthinkable without CSS. It has its pitfalls, but when used correctly it’s damn powerful. It’s always incredible what developers do with it. This month’s Discoveries is about the basics and the amazing. My Custom CSS ResetDefensive CSS10 Useful CSS Tricks for Front-end DevelopersAnimated Star RatingCSS Marquee ExamplesCSS Rolling TextCool Hover Effects That Use CSS Text ShadowSolving 'The Dangler' Conundrum with Container Queries and :has()Conditionally Styling Selected Elements in a Grid ContainerIntersection Observer Scrolling Effects My Custom CSS Reset by Josh Comeau https://www.joshwcomeau.com/css/custom-css-reset/ Browsers behave differenly out of the box regarding CSS. Therefore it is always advisable to have a CSS reset. Josh shows us his approach. He also inspired Elly to her Gist. Defensive CSS by Ahmad Shadeed https://defensivecss.dev/ Ahmad is a master of CSS and one of his concerns, which he also points out repeatedly in his blog, is to use styles defensively. He has now made his own website out of this. 10 Useful CSS Tricks for Front-end Developers by Alex Ivanovs https://stackdiary.com/useful-css-tricks/ Alex has some useful tips on writing better and smart CSS code. I have to try Shadow for transparent images as soon as possible … Animated Star Rating|IMGFILE by Jon Kantner https://codepen.io/jkantner/pen/BarvVNa Star ratings are everywhere because they encourage the user to interact with the website and may give the following important clues about how good the product is. Jon has taken them to the next level visually with his animations. CSS Marquee Examples by Ryan Mulligan https://codepen.io/hexagoncircle/full/eYMrGwW Marquee visualizations were all the rage in the 90s, but they still have their place today and are easier to implement than ever before. CSS Rolling Text by Marcello Lopes https://codepen.io/marcell0lopes/pen/oNemQmB You have to tease something with more than one verb? Try this simple solution from Marcello on scrolling text with pure CSS. Cool Hover Effects That Use CSS Text Shadow by Temani Afif https://css-tricks.com/cool-hover-effects-that-use-css-text-shadow/ Hover effects are only useful on computers with mouse support, but how cool you can design them Temani shows us on CSS Tricks. Solving 'The Dangler' Conundrum with Container Queries and :has() by Dave Rupert https://daverupert.com/2022/07/solving-the-dangler-conundrum-with-has-and-container-queries Grids are super cool, but there is the problem how to style leftovers that doesn’t fit in the matrix. Having a 3 column grid and 12 elements? Fine. But what is with the 13th element? Dave shows us how to deal it it in CSS. Conditionally Styling Selected Elements in a Grid Container by Preethi https://css-tricks.com/conditionally-styling-selected-elements-in-a-grid-container/ Elements in a grid have their style. Period. … Wait, no! It is possible to style an element regarding to its neighbour, as shown in this article, by a clever using of :nth-of-type. Intersection Observer Scrolling Effects by Jhey https://codepen.io/jh3y/pen/xxWyEBQ Codepen’s user JHey shows us how to animate elements in 5 different ways on scrolling. Pretty neat…","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"UI","slug":"UI","permalink":"https://kiko.io/tags/UI/"}]},{"title":"One mouse to rule them all","subtitle":null,"series":"Golem","date":"2022-10-08","updated":"2022-10-08","path":"post/One-mouse-to-rule-them-all/","permalink":"https://kiko.io/post/One-mouse-to-rule-them-all/","excerpt":"Multiple monitors on one computer are elementary for effective IT work. However, operating multiple computers with one input set requires tools, for example one from the Microsoft Garage. The trend is toward second or third devices, especially among IT workers. In addition to the stationary computer, which may still be under the desk, there is a laptop and possibly a tablet. All of them are connected to cloud services. If the desk is large enough, there may be two monitors and a full-size keyboard connected to the desktop, the laptop on the left, on which two or three special programs are installed, and the Surface tablet on the right for communication with its narrow-track keyboard. And spread out on the table are three mice in different colors so as not to constantly get the wrong one. A scenario that the author of these lines was also confronted with … yes, until a few years ago a small tool for Microsoft Windows fell in front of his feet, which abruptly ended the input chaos: Mouse Without Borders.","keywords":"multiple monitors computer elementary effective work operating computers input set requires tools microsoft garage trend devices workers addition stationary desk laptop possibly tablet connected cloud services large full-size keyboard desktop left special programs installed surface communication narrow-track spread table mice colors constantly wrong scenario author lines confronted … years ago small tool windows fell front feet abruptly ended chaos mouse borders","text":"Multiple monitors on one computer are elementary for effective IT work. However, operating multiple computers with one input set requires tools, for example one from the Microsoft Garage. The trend is toward second or third devices, especially among IT workers. In addition to the stationary computer, which may still be under the desk, there is a laptop and possibly a tablet. All of them are connected to cloud services. If the desk is large enough, there may be two monitors and a full-size keyboard connected to the desktop, the laptop on the left, on which two or three special programs are installed, and the Surface tablet on the right for communication with its narrow-track keyboard. And spread out on the table are three mice in different colors so as not to constantly get the wrong one. A scenario that the author of these lines was also confronted with … yes, until a few years ago a small tool for Microsoft Windows fell in front of his feet, which abruptly ended the input chaos: Mouse Without Borders. Microsoft, like many IT corporations, encourages its employees to create side projects, which then either eventually find their way into one of the main products or are released under the Microsoft Garage banner. This is also the case with the work of Truong Do, a Microsoft employee from Washington, who deals with Microsoft Dynamics in his daily business. He was probably also fed up with the numerous input devices and developed a tool more than 10 years ago with which the mouse pointer of a Windows device can be moved across its own monitor border to another device, whereby one of the keyboards is then activated on the device on which the mouse pointer is currently located. The startup is simple: Download tool via http://aka.ms/mm Make sure that all devices are available on the same network Run the installation program on all devices From one device, link the other devices using the security code generated by the program Arrange the devices on the desktop in the program by dragging and dropping them from their locations Done Other featuresBesides the main purpose of sharing mouse and keyboard across multiple devices, you can also set up a shared clipboard (Share Clipboard) and make it possible to copy files from one computer to another via drag & drop (Transfer File). This creates a special folder on the desktop of the target computer, where these files end up via the clipboard. It is also useful that the user does not become a long-distance pusher when the pointer is on the far right device and he has to go to the far left, because where it stops on the right, it continues on the far left. For keyboard lovers, Truong Do has made keyboard shortcuts available for each linked device, with a choice of F-keys or numbers. All machines can also be locked at once via Ctrl-Alt-L. If the connection hangs once, a spirited Ctrl-Alt-R helps to get them all back on track. As an extra goodie, Truong Do has included a narrow-gauge screenshot capture, but you can safely turn it off, because it can’t compete with tools like Brian Scott’s Cropper or Greenshot, but rather hinders because it uses common keyboard shortcuts. ConclusionYes, multiple large monitors and classic remote solutions like RDP or TeamViewer help to operate multiple machines at the same time, but when you have the zoo of devices physically together, working with Mouse Without Borders feels like having four or more monitors, with the advantage that the individual machines don’t have to give up any of their performance. The nice thing, too, is that it doesn’t matter at all what version of Windows is running on the devices. Everything from Windows 7 upwards is supported.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Remote","slug":"Remote","permalink":"https://kiko.io/tags/Remote/"},{"name":"Workflow","slug":"Workflow","permalink":"https://kiko.io/tags/Workflow/"}]},{"title":"Creating Icon Font from SVG Files","subtitle":null,"date":"2022-09-17","updated":"2022-09-17","path":"post/Creating-Icon-Font-from-SVG-Files/","permalink":"https://kiko.io/post/Creating-Icon-Font-from-SVG-Files/","excerpt":"A several years ago I started building a little PWA and chose Bootswatch 3.3.5. for theming. As it depends on Bootstrap I was able to use the icons from Bootstrap. At the beginning I needed only a handful of these icons, but with the time it became more and more difficult to find the right one, because the Bootstrap Glyphicons in version 3 included only around 250 icons and there was not always the right one. Also, the app was always lugging around well over 100 KB of extra files, of which I actually only needed a few kilobytes. In another project I had used Fontello, where you can build and download your own icon font from a selection of available icons. Very nice, but I didn’t feel like fiddling with project-specific configuration files on the Fontello website. But since you could upload your own SVG files in Fontello, which were then taken over into the font, the same had to work somehow with a Node.JS plugin!? And yes gulp-iconfont from Nicolas Froidure was exactly what I needed. First SolutionJust copy a bunch of SVG files in a folder, run gulp and there was my own customized icon font with a tolerable size of around 20 kilobytes. At that time, Thomas Jaggi had taken care of the creation of a CSS file with the correct code points that matched the font with his tool gulp-iconfont-css.","keywords":"years ago started building pwa chose bootswatch theming depends bootstrap icons beginning needed handful time difficult find glyphicons version included app lugging kb extra files kilobytes project fontello build download icon font selection nice didnt feel fiddling project-specific configuration website upload svg work nodejs plugin gulp-iconfont nicolas froidure solutionjust copy bunch folder run gulp customized tolerable size thomas jaggi care creation css file correct code points matched tool gulp-iconfont-css","text":"A several years ago I started building a little PWA and chose Bootswatch 3.3.5. for theming. As it depends on Bootstrap I was able to use the icons from Bootstrap. At the beginning I needed only a handful of these icons, but with the time it became more and more difficult to find the right one, because the Bootstrap Glyphicons in version 3 included only around 250 icons and there was not always the right one. Also, the app was always lugging around well over 100 KB of extra files, of which I actually only needed a few kilobytes. In another project I had used Fontello, where you can build and download your own icon font from a selection of available icons. Very nice, but I didn’t feel like fiddling with project-specific configuration files on the Fontello website. But since you could upload your own SVG files in Fontello, which were then taken over into the font, the same had to work somehow with a Node.JS plugin!? And yes gulp-iconfont from Nicolas Froidure was exactly what I needed. First SolutionJust copy a bunch of SVG files in a folder, run gulp and there was my own customized icon font with a tolerable size of around 20 kilobytes. At that time, Thomas Jaggi had taken care of the creation of a CSS file with the correct code points that matched the font with his tool gulp-iconfont-css. SetupFirst I needed some SVG files… 12345678|-- Images |-- SVG |-- alert.svg |-- cancel.svg |-- delete.svg |-- email.svg |-- flag.svg |-- link.svg … and the NPM packages of my task runner gulp.js: 12npm install --save-dev gulp-iconfontnpm install --save-dev gulp-iconfont-css For generating the CSS file a template called _icons.css is shipped with the NPM package of gulp-iconfont-css, which can be customized as needed. In my case I just made a copy into a folder called Templates, where I store all other template files I need for my PWA. Templates/icons-template.css1234567891011121314151617181920212223242526@font-face { font-family: "<%= fontName %>"; src: url('<%= fontName %>.eot'); src: url('<%= fontName %>.eot?#iefix') format('eot'), url('<%= fontName %>.woff2') format('woff2'), url('<%= fontName %>.woff') format('woff'), url('<%= fontName %>.ttf') format('truetype'), url('<%= fontName %>.svg#<%= fontName %>') format('svg');}.<%= cssClass %>:before { font-family: "<%= fontName %>"; font-style: normal; font-variant: normal; font-weight: normal; text-decoration: none; text-transform: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}<% _.each(glyphs, function(glyph) { %>.<%= cssClass %>-<%= glyph.fileName %>:before { content: "\\<%= glyph.codePoint %>";}<% }); %> As I was using Gulp for building my app, I had to integrate a new task for creating the icon font in the gulpfile.js: /gulpfile.js12345678910111213141516171819202122232425262728293031var iconfont = require('gulp-iconfont'), iconfontCss = require('gulp-iconfont-css');var distFolder = 'Build';var fontName = 'MyAppIcons'; gulp.task('create-iconfont', function(){ return gulp.src(['Images/SVG/*.svg']) .pipe(iconfontCss({ fontName: fontName, path: 'Templates/icons-template.css', targetPath: fontName + '.css', fontPath: distFolder })) .pipe(iconfont({ formats: ['svg', 'ttf', 'eot', 'woff', 'woff2'], fontName: fontName, // Required fontHeight: 1001, normalize: true, prependUnicode: true, // Recommended timestamp: Math.round(Date.now() / 1000) // Recommended })) .pipe(gulp.dest(distFolder)); });gulp.task('build', gulp.series( 'create-iconfont' // other gulp tasks )); I little explanation on that … First of all the task for gulp-iconfont-css has to be inserted before piping the files through gulp-iconfont, in order to create the CSS file properly. Following options were used: gulp-iconfont-css Option Description fontName Name that the generated font will have path Path to the template for generating CSS file targetPath Path where the CSS file will be generated fontPath Path to the icon font file gulp-iconfontThe library combines some other projects such as svgicons2svgfont, gulp-svgicons2svgfont and gulp-svg2ttf to create the different font formats, because its just a wrapper around them. Option Description formats Font file formats that will be created fontName Name of the font (for svgicons2svgfont) fontHeight Minimum height on scaling icons normalize Normalize icons by scaling them to the height of the highest icon (for svgicons2svgfont) prependUnicode Prefix files with their automatically allocated unicode code point (for gulp-svgicons2svgfont) timestamp Get consistent builds when watching files (for gulp-svg2ttf) OutputAfter running gulp build all files needed were generated: 1234567|-- Build |-- MyAppIcons.css |-- MyAppIcons.eot |-- MyAppIcons.svg |-- MyAppIcons.ttf |-- MyAppIcons.woff |-- MyAppIcons.woff2 MyAppIcons.css1234567891011121314151617181920212223242526272829303132333435363738394041424344@font-face { font-family: "MyAppIcons"; src: url('MyAppIcons.eot'); src: url('MyAppIcons.eot?#iefix') format('eot'), url('MyAppIcons.woff2') format('woff2'), url('MyAppIcons.woff') format('woff'), url('MyAppIcons.ttf') format('truetype'), url('MyAppIcons.svg#MyAppIcons') format('svg');}.icon:before { font-family: "MyAppIcons"; font-style: normal; font-variant: normal; font-weight: normal; text-decoration: none; text-transform: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}.icon-alert:before { content: "\\E001";}.icon-cancel:before { content: "\\E002";}.icon-delete:before { content: "\\E003";}.icon-email:before { content: "\\E004";}.icon-flag:before { content: "\\E005";}.icon-link:before { content: "\\E006";} All I had to do now, was to reference the new CSS file in my HTML and decorating my HTML tags with one of the new icon-classes. Yes! My had my own icon font, just by copying some SVG files in a folder… The ProblemA while later, as my app grew, I needed some new icons, but in the meantime I had used the content codes elsewhere in static CSS files, for example with other pseudo-selectors like AFTER. style.css1234.my-special-link::after { font-family: "MyAppIcons"; content: "\\E006";} After copying a few new icon files to the SVG folder and running the build, I found that most of the icons didn’t fit anymore!? After a short research it was clear to me what had happened. With the insertion of the new SVG files, the order of the files in the folder was changed. But since the libraries processed this folder in alphabetical order, file by file, and simply incremented the codes to be assigned, the codes had simply shifted. In the example above, this can be easily understood if we insert a file named cloud.svg here. The code for cancel.svg (E002) remains untouched, but cloud.svg now gets E003 and delete.svg E004 and so on. The hardwired icon for .my-special-link (E006) now showed instead of the link icon a flag icon. Newer SolutionI don’t know when this happened, because I don’t keep a constant eye on my little PWA and thus the further development of the related tools, but after having manually adjusted the moved codes two or three times now and once again wanting to change a few things on the PWA’s icons, I had enough and came across the following sentence on the gulp-iconfont-css page on GitHub: Recent versions of gulp-iconfont emit a glyphs (or codepoints < 4.0.0) event (see docs) which should likely be used instead of the workflow described below. However, it will continue to work as expected. --- backflip (Thomas Jaggi) Ahh, ok. I don’t need the gulp-iconfont-css anymore. I can create the CSS file by myself. Lets see how the gulp-iconfont task looks, after rewriting: /gulpfile.js1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950var iconfont = require('gulp-iconfont');var distFolder = 'Build';var fontObj = { fontName: "MyAppIcons", cssClass: "icon"};gulp.task("create-iconfont", function () { return gulp .src(["Images/SVG/*.svg"]) .pipe( iconfont({ fontName: fontObj.fontName, fontHeight: 1001, formats: ["svg", "ttf", "eot", "woff", "woff2"], normalize: true, prependUnicode: true, timestamp: Math.round(Date.now() / 1000), }) ) .on("glyphs", function (glyphs, options) { fontObj.glyphs = glyphs.map(mapGlyphs); console.log(fontObj, options); gulp .src('Templates/icons-template.css') .pipe(consolidate("lodash", fontObj)) .pipe(rename({ basename: fontObj.fontName })) .pipe(gulp.dest(distFolder)); }) .pipe(gulp.dest(distFolder));});function mapGlyphs(glyph) { return { fileName: glyph.name, codePoint: glyph.unicode[0].charCodeAt(0).toString(16).toUpperCase(), };}gulp.task('build', gulp.series( 'create-iconfont' // other gulp tasks )); First I have introduced a new fontObj variable to hold all informations for the untouched CSS template. The major difference is now, that iconfont is the only main task, with a subtask where the glyphs are processed directly via consolidate. The mapGlyphs() function ensures that the file names with the CodePoints are transferred into an easily usable structure. Works fine, but doesn’t solve my problem with the shifted CodePoints, when inserting new SVG icons … but someone remarked in a StackOverflow article to have a look at the test files of gulp-iconfont. There, the SVG files carry as prefix the name of the CodePoints to be used for this file!. Unfortunately, this feature is not documented, but it works… 123456789|-- Images |-- SVG |-- uE001-alert.svg |-- uE002-cancel.svg |-- uE003-delete.svg |-- uE004-email.svg |-- uE005-flag.svg |-- uE006-link.svg |-- uE007-cloud.svg Now the order of the SVG files doesn’t matter anymore. I just copy a new SVG file into the folder and rename it with a currently unused Unicode. Upcoming Solution: Using SVG SpritesInterestingly, the inventor of gulp-iconfont page writes on the GiHub page: Warning: While this plugin may still be useful for fonts generation or old browser support, you should consider using SVG icons directly. Indeed, when i created gulp-iconfont and all its related modules, using SVG icons was just not realistic for a wide browser suppport but i was already conviced that SVG was the future, that’s why i wanted my SVG source files to sit separated in a folder. So, now, just enjoy switching to SVG with almost no effort :). Was a great open source journey with you all! --- Nicolas Froidure (nfroidure) This goes along with some older articles of Chris Coyer from CSSTricks: https://css-tricks.com/svg-sprites-use-better-icon-fonts/ https://css-tricks.com/svg-symbol-good-choice-icons/ https://github.com/w0rm/gulp-svgstore Maybe it’s time to say goodbye to icon fonts completely and use SVG files directly, but in my opinion this is not suitable for all needs. Especially if you work a lot with pesudo selectors, a general rebuild of the code and the UI will be necessary and you would do well to weigh the cost-benefit carefully.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Bundling","slug":"Bundling","permalink":"https://kiko.io/tags/Bundling/"},{"name":"SVG","slug":"SVG","permalink":"https://kiko.io/tags/SVG/"},{"name":"Font","slug":"Font","permalink":"https://kiko.io/tags/Font/"}]},{"title":"Dopamine, a music player for Windows 10 as it should be","subtitle":"A minimalist MP3 player under Windows, distributed as open source, which does not shy away from large collections","series":"Golem","date":"2022-08-21","updated":"2022-08-21","path":"post/Dopamine-a-music-player-for-Windows-10-as-it-should-be/","permalink":"https://kiko.io/post/Dopamine-a-music-player-for-Windows-10-as-it-should-be/","excerpt":"There are about fourleventy millions music apps for smartphones running Android and iOS. However, most of them are relatively junk or try to foist malware on users. You have to make sure that you separate the wheat from the chaff. On the other hand, the situation is surprisingly different for Windows, the much older operating system. The older ones of us will still remember the glorious WinAmp times, whose current owner Radionomy has been making a very long new attempt for a new version 6 since 2018 (version 5.8 is already a handsome 6 years old), but you can actually count the good music players for Windows 10 and higher on one hand, if you subtract the streaming apps such as Spotify and Co. and disregard everything that comes along as a jack of all trades and can ALSO play MP3. The best known are the in Windows included and miserably failed iTunes clone from Micosoft called Groove, AIMP, foobar2000, MediaMonkey and MusicBee. Some nostalgic people might also add the good old Windows Media Player, which managed to survive on the net despite Groove. If you look at the download pages of these music player candidates and try to look behind the business model, some of them simply do not download. One or the other player also overdoes it with the featuritis. Bouncing balls or bars to the music are gimmicks that were thought to be outdated long ago, when it is actually only about listening to music. Belgian software developer Raphaël Godart (twitter.com/RaphaelGodart) must have felt the same way a few years ago when he set out to launch his own player for local MP3 collections, which in this case sounds falsely commercial because his player Dopamine is freely available on GitHub and open source under GPL 3.0 license. It plays music under a plain and simple, yet chic interface … Period. Everything a music lover’s heart desires is on board:","keywords":"fourleventy millions music apps smartphones running android ios junk foist malware users make separate wheat chaff hand situation surprisingly windows older operating system remember glorious winamp times current owner radionomy making long attempt version handsome years count good players higher subtract streaming spotify disregard jack trades play mp3 included miserably failed itunes clone micosoft called groove aimp foobar2000 mediamonkey musicbee nostalgic people add media player managed survive net download pages candidates business model simply overdoes featuritis bouncing balls bars gimmicks thought outdated ago listening belgian software developer raphaël godart twittercom/raphaelgodart felt set launch local collections case sounds falsely commercial dopamine freely github open source gpl license plays plain simple chic interface … period lovers heart desires board","text":"There are about fourleventy millions music apps for smartphones running Android and iOS. However, most of them are relatively junk or try to foist malware on users. You have to make sure that you separate the wheat from the chaff. On the other hand, the situation is surprisingly different for Windows, the much older operating system. The older ones of us will still remember the glorious WinAmp times, whose current owner Radionomy has been making a very long new attempt for a new version 6 since 2018 (version 5.8 is already a handsome 6 years old), but you can actually count the good music players for Windows 10 and higher on one hand, if you subtract the streaming apps such as Spotify and Co. and disregard everything that comes along as a jack of all trades and can ALSO play MP3. The best known are the in Windows included and miserably failed iTunes clone from Micosoft called Groove, AIMP, foobar2000, MediaMonkey and MusicBee. Some nostalgic people might also add the good old Windows Media Player, which managed to survive on the net despite Groove. If you look at the download pages of these music player candidates and try to look behind the business model, some of them simply do not download. One or the other player also overdoes it with the featuritis. Bouncing balls or bars to the music are gimmicks that were thought to be outdated long ago, when it is actually only about listening to music. Belgian software developer Raphaël Godart (twitter.com/RaphaelGodart) must have felt the same way a few years ago when he set out to launch his own player for local MP3 collections, which in this case sounds falsely commercial because his player Dopamine is freely available on GitHub and open source under GPL 3.0 license. It plays music under a plain and simple, yet chic interface … Period. Everything a music lover’s heart desires is on board: Automatic reading of a folder configured at the beginning. Display of the collection by artist, genre, album, title or folder Playlist management Light and dark theme, including setting the accent color Interface in 30 languages Integration with taskbar and notification bar Automatic updates tns({ container: \"#image-slide-d1y32p\", items: 1, slideBy: \"page\", controls: false, nav: true }); The design of the application is instantly convincing. Everything is limited to the most necessary, an accent color and a lot of white e.g. gray space, so that not only the ear feels comfortable with the music, but also the eye. And all this at a very good speed and nicely animated transitions from one view to another. Besides the basic ability to play MP3 files, the author has also included a rudimentary, but well-functioning editing function of the MP3 metadata. Furthermore, any star assignments or added lyrics are stored directly in the file and not just in the SQLite database behind it. However, Godart has also included a few useful things from the Featuritis department in its program. On the one hand, it downloads information from last.fm about the currently played artist and you can scrobble there if you like, and on the other hand, it scans some lyric collections for the lyrics, which unfortunately rarely works. Quite unique is one of the most recently added features: a blacklist! Yes, many an album of a favorite musician contains a track that he would have been better off sparing himself. Such pieces can now be specifically hidden, so that the musical enjoyment is not spoiled. Further developmentNew releases of Dopamine 2 since version 2.0.8 are not as frequent as before, because the program is basically developed out and Raphaël only adds smaller features, translations and bugfixes. Instead, 4 years after the release of the first version two years ago, he has embarked on a completely new development with version 3. Where Dopamine up to version 2 was developed with Microsoft C# and WPF (Windows Presentation Foundation), the latest will be based on Electron, Angular and Typescript, i.e. packaged web technology (HTML, CSS and JavaScript) for all operating systems and not just Windows. Currently, Godart has reached Preview 10, which has by far not the same scope of the previous version, but is already running stable. Music in the cloudIf you’re on the go and have stored your music on a cloud service like Microsoft’s OneDrive, you might pay attention to the fact that on a device connected to it, the entire collection is not downloaded, but only selected parts in order to save local storage space. Microsoft makes it easy for users there: it displays all OneDrive files in Windows Explorer with 0 bytes of space used and only downloads them when they are accessed or when the user selects Always keep on this device in a folder’s context menu. This works quite beautifully with Dopamine 2. It, with support from C# and the operating system, simply ignores all those files that are not available on the disk in real terms when scanning the specified music folder. Dopamine 3, on the other hand, in which the scan must take place via Node.js and the fs.readdir() method, immediately triggers the download function of OneDrive and you unintentionally fill up your disk. Curiosity on the sideRaphaël’s C#/WPF solution weighs 76 MB when installed, 2.5 MB of it for the executable, the new Electron-based one under Windows, however, already 302 MB, with the EXE being a whopping 128 MB. If you take a look at the code base, this becomes even more obvious: while the Windows-only version comes along with 760 files, the new Electron version already contains 86,092 files in 7,948 folders with a total weight of almost one gigabyte after running npm install! Once again you can see that platform independence in the brave new Node.JS world is often bought with heavy weight. This is mainly due to the necessity (or the unkindness, depending on how you look at it) to drag along thousands of dependent libraries (dependencies), which occasionally only bring a string to a certain width or something similarly trivial. Not to mention the problems with depublicated or malware-infested dependencies, which have recently upset large parts of the community. ConclusionDopamine 2 is a really fantastic music player for Windows that simply outshines most others due to its simplicity and well-done interface. Let’s see if Godard manages to do something similar with version 3 and Electron. It’s a challenge, but he seems to be a full-blooded developer. Chapeau Raphaël…","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Audio","slug":"Audio","permalink":"https://kiko.io/tags/Audio/"}]},{"title":"Scandinavian Presets for Lightroom","subtitle":null,"series":"Lightroom Presets","date":"2022-07-19","updated":"2022-07-19","path":"post/Scandinavian-Presets-for-Lightroom/","permalink":"https://kiko.io/post/Scandinavian-Presets-for-Lightroom/","excerpt":"It doesn’t matter what time of year you go to Scandinavia. Countries like Denmark or Norway have a Nordic charm that you can never really escape. But it doesn’t always have to be the far north that provides fascinating motifs for photographers. With these Lightroom presets you can enhance your images with the Nordic feel.","keywords":"doesnt matter time year scandinavia countries denmark norway nordic charm escape north fascinating motifs photographers lightroom presets enhance images feel","text":"It doesn’t matter what time of year you go to Scandinavia. Countries like Denmark or Norway have a Nordic charm that you can never really escape. But it doesn’t always have to be the far north that provides fascinating motifs for photographers. With these Lightroom presets you can enhance your images with the Nordic feel. Scandinavian City NightsWhen it gets late in the city, the artificial light bathes them in a warm veil. The shadows are deeper and the colors are more concise.Time to get a drink. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-42uwqs\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scandinavian City Nights.xmp Scandinavian Blue HourAll over the world, the Blue Hour is a special time of the day. The light is fading away slightly and everything shines in magic colors. Let them shine… var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-zdvvll\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scandinavian Blue Hour.xmp Scandinavian ColorsNordic nature is far from being as rich and colorful as that in the south, but it has its own charm and with color you can help a little. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-pzketg\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scandinavian Colors.xmp Scandinavian DramaNot only since Shakespeare and his Hamlet, we know about the dramas of the Nordic sagas. The landscape itself is dramatic and the stories are set in it. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-1jso0k\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scandinavian Drama.xmp Scandinavian SeascapeThe sea plays a big role in the Nordic countries, as they have very long coasts. No one who travels there with a camera manages to resist this beauty. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-b9ngqb\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scandinavian Seascape.xmp","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Presets","slug":"Presets","permalink":"https://kiko.io/tags/Presets/"}]},{"title":"Gitpod - Visual Studio Code on the Web","subtitle":"The popular code editor is conquering the browser and remote work and the Kiel-based company Gitpod is at the forefront with their solution","series":"Golem","date":"2022-07-17","updated":"2022-07-17","path":"post/Gitpod-Visual-Studio-Code-on-the-Web/","permalink":"https://kiko.io/post/Gitpod-Visual-Studio-Code-on-the-Web/","excerpt":"It’s amazing how quickly the editor Visual Studio Code (VS Code) has conquered the developer community (#1 in the Stack Overflow Developer Survey Ranking 2021) and even those from the Linux faction, who are historically rather critical of Microsoft, but for good reason. The company from Redmond has done quite a lot right with the tool and has gathered a large group of open source developers around it (currently 1,640 contributors), who contribute to the fact that the Swiss Microsoft team around Erich Gamma can bring out a new release for Windows, Linux and macOS every few weeks. It all started with the Monaco Editor, the core around which VS Code is built and which was first released on April 14, 2016. The exciting thing about Monaco and VS Code is, that it is consistently written using web technologies, i.e. HTML, CSS and Javascript, packaged and executed using the framework Electron developed by GitHub, which in turn is based on Node.js and the open source browser engine Chromium from Google.","keywords":"amazing quickly editor visual studio code conquered developer community #1 stack overflow survey ranking linux faction historically critical microsoft good reason company redmond lot tool gathered large group open source developers contributors contribute fact swiss team erich gamma bring release windows macos weeks started monaco core built released april exciting thing consistently written web technologies html css javascript packaged executed framework electron developed github turn based nodejs browser engine chromium google","text":"It’s amazing how quickly the editor Visual Studio Code (VS Code) has conquered the developer community (#1 in the Stack Overflow Developer Survey Ranking 2021) and even those from the Linux faction, who are historically rather critical of Microsoft, but for good reason. The company from Redmond has done quite a lot right with the tool and has gathered a large group of open source developers around it (currently 1,640 contributors), who contribute to the fact that the Swiss Microsoft team around Erich Gamma can bring out a new release for Windows, Linux and macOS every few weeks. It all started with the Monaco Editor, the core around which VS Code is built and which was first released on April 14, 2016. The exciting thing about Monaco and VS Code is, that it is consistently written using web technologies, i.e. HTML, CSS and Javascript, packaged and executed using the framework Electron developed by GitHub, which in turn is based on Node.js and the open source browser engine Chromium from Google. It is not really surprising that the developers have built a really good editor from the start, which could directly defy the first Chromium-based tool Brackets from Adobe at that time, because Erich Gamma was the head of the development environment Eclipse for many years and therefore knows about the needs of the worldwide developer community. Surprisingly however is already, that although VS Code is based on Web technologies, it took some years, until the editor made it out of its Electron cardboard, directly into the Browser. For example, it wasn’t until 2020, 2 years after Microsoft acquired GitHub, that they announced github.dev, which is just a call target for the newly introduced Magic Dot and opens a project hosted on GitHub in the browser. For those who like to try it out, just go to any GitHub repository and hit the DOT key and the project will open in a VS Code browser window. A variant of this is vscode.dev, which can open not only GitHub projects, but any from your own hard drive. However, both browser editors have one thing in common: you can code wonderfully on the go, but you cannot start the projects and thus also not validate your own code for executability. The substructure is missing and is therefore not a real IDE. No pre-processing via Grunt or Gulp, no starting web server or similar, simply because there is no console that could communicate with the operating system. However, Microsoft has also announced another tool in 2020 that is supposed to bring exactly this substructure via cloud container: GitHub Codespaces. Beta access has been expanding for a few weeks now, and it should be available for team and enterprise cloud plans starting in August. Cloud-based development environment from Kiel, Germany: GitpodA team around Sven Efftinge and Anton Kosyakov from the beautiful city of Kiel, asked themselves the question why not build an online development environment back in 2017 and launched the project Theia under the umbrella of the Eclipse Foundation. The idea was to develop a remote-first IDE that runs both locally and in the browser and fully supports the numerous VS Code extensions already available. They named the actual product around Theia Gitpod (gitpod.io). Although there are still numerous Theia solutions available today, such as Eclipse Che, Stackblitz or the Google Cloud Shell Editor, Gitpod decided to move to the VS Code platform in late 2020 after Erich Gamma and his team provided Remote Support in VS Code, on which github.dev is based, among others. However, since Microsoft had decided not to open source the server component for the time being (the commercial GitHub CodeSpaces sends its regards), Anton, by his own account, created a first working version of the Open VSCode Server in 4 days, which was released in September 2021. Not a month later, Microsoft also made its own server solution freely available, but with a few restrictions, among other things, regarding the extensions that can be obtained from Gitpod via the Open VSX Registry portal, since the Microsoft Extension Marketplace is reserved for Microsoft’s own products. The name and the technology behind itThe name gitpod already tells us something about the technology behind the service. ‘git’ stands for the file management system behind the editor and ‘pod’ for the server technology under the editor. git…Nowadays, source code is (hopefully) no longer just stored locally on a hard disk, but centrally and managed by source code management software. The current gold standard in this area is the free Git initiated by Linus Torvalds in 2005, a distributed version management that forms the basis of popular developer platforms such as GitHub, GitLab or Bitbucket. …podA short step back in time: It’s not so long ago that IT’ers called an environment on which they published a server application machines, because it first needed hardware with processor, main memory, hard disks and an operating system of choice, which was then installed and on that in turn the said application. However, if the server application was only used sporadically, the expensive hardware was underutilized, i.e. resources were wasted. To solve this problem, work began in the 1970s on virtualizing hardware so that several logically separate systems could share the hardware to increase its utilization. Any operating system and the required server applications can be installed and operated in each of the virtual machines (VM’s) created in this way. In this concept, however, there are redundancies in the form of the operating system. If, for example, you want to run 5 Linux-based web servers, you will also have to loop along 5 possibly identical Ubuntu installations that want to be maintained. Docker is not the only, but most well-known software that addressed this problem in 2013 by abstracting another layer, in this case the operating system. Here we are now no longer talking about a machine or VM, but a container in which the server application runs. For the orchestration of these containers, the software Kubernetes initiated by Google has been available since 2015, in which the smallest deployable unit is called a pod, which contains one or more containers that share the allocated resources. Such a pod with a ready installed and configured Open VSCode Server is launched on Google Cloud Platform when a user opens a Gidpod Workspace. Let’s go … Starting a Gitpod WorkspaceIn Gitpod, you launch a project hosted on Github, Gitlab or Bitbucket into a so-called workspace. To do this, simply prepend the url of your repository with the string gitpod.io/#. Those who prefer buttons, can either use a provided bookmarklet and resort to a browser extension that adds a Gitpod button to the Github interface. Once the workspace has been started and you have closed the browser window, it will be available for a while on the dashboard under Workspaces. If you work frequently with the same project, it is advisable to create a permanent shortcut there under Projects if you are not into manual url changes or buttons. Last but not least, the icing on the cake is to install one of these Gitpod projects as a Chrome App and drop it in the toolbar. That way, the code is just a click away and the whole thing almost feels like a local VS Code due to the reduced browser window. Configuration of the workspace of a projectIn addition to the general setting options, Gitpod offers an individual configuration per project. For this the file .gitpod.yml is searched in the root of the project folder and used. All setting options are well documented in the gitpod Docs. Using a custom containerThe default container that Gitpod boots up when starting a workspace is based on Debian/Ubuntu and already includes a lot of frameworks and languages like Node, Java, Go, Python and some more. However, if you want to use a different image, you can set this via the image entry, either by referencing a public image or by specifying the name of a Dockerfile in the project. The possibilities here are numerous and can be found in the section Custom Docker Image. Tasks at startupTo get a project running in Visual Studio Code, especially in the Node environment, there are a few things that need to be set up, such as installing the correct Node.js version and dependent packages using NPM or another package manager. The same is true for Gitpod, of course, although these actions need to be done again and again after the working environment has been started, for example, if the pod has been discarded after a while. For these recurring tasks, the software provides the Tasks section in the .gitpod.yml and there, in the forefront, the init entry. In the following example, Node 14.17, all local packages and one global package are installed as a multi-line task: 12345tasks: - init: | nvm install 14.17.2 npm install npm install -g grunt-cli With the grouping and naming of tasks, the terminal display settings and the total of three execution levels before, init and command it is easy to create a configuration that starts up the workspace in a fixed way while keeping an overview. If you like it even a bit faster, you can use so-called Prebuilds, which serve as a snapshot for creating a new workspace. These prebuilds use the .gitpod.yml of the project and are closely linked to the source code management used (currently GitHub, GitLab and Bitbucket). Thus, a prebuild is recreated each time modified code is checked into the project. It doesn’t get much faster and more convenient to code from anywhere in the browser. Include extensions automaticallyWhen creating the gitpod workspace, the configuration file .gitpod.yml can also be used to include the Visual Studio code extensions that are needed for working. The easiest way to do this is if the extension is represented on the open platform Open VSX Registry, because gitpod looks for the pattern ${publisher}.${name} there by default. Example: 1234vscode: extensions: - HookyQR.beautify - kamikillerto.vscode-colorize However, VSIX files from other sources can also be included via the full url. Microsoft offers with the Visual Studio Marketplace the primary and largest source of extensions, but unfortunately omits the specification of a complete download path of the VSIX file. However, it is very easy to build this using the following pattern: https://${publisher}.gallery.vsassets.io/_apis/public/gallery/publisher/${publisher}/extension/${extension}/${version}/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage The necessary information about the variables Publisher, Extension and Version in this pattern can be obtained from the details page of an extension in Visual Studio Code. Output: 123456Name: vscode-hexo-utilsId: fantasy.vscode-hexo-utilsDescription: vscode extension for hexoVersion: 0.2.1Publisher: fantasyVS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=fantasy.vscode-hexo-utils This will result in the following entry in .gitpod.yml: 123vscode: extensions: - https://fantasy.gallery.vsassets.io/_apis/public/gallery/publisher/fantasy/extension/vscode-hexo-utils/0.2.1/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage Synchronize settingsEvery IDE looks different, depending on the tastes and preferences of the developer sitting in front of it, and so relatively quickly after the first release of VS Code, there were the first extensions that could synchronize the IDE’s settings across multiple machines (mostly via Gists), until Microsoft took on the feature and integrated it directly into the IDE. Now, with Gitpod, we have a new but slightly different instance of Visual Studio Code, but the Kielers thought of that, too. They couldn’t access the data of the integrated synchronization, but they created a by-pass with their own extension that works just as well. After the installation in VS Code and a restart of the same, one logs in to the extension with the same account under which one has registered with Gitpod and can thereafter synchronize tasks, code snippets extensions and keyboard shortcuts in addition to the settings. Prices and ServicesGitpod is a company and has to look like all others, how it can work cost-covering. Therefore, as is often the case, there are different plans that can be booked. There are four of them at Gidpod in the Saas area, whereby Free differs from the other plans only in the possible computing time, the timeout and the parallel running workspaces. Thus, in the Free plan, there are 50 hours that you can work with 4 WorkSpaces and in case of inactivity, the pods are shut down again after 30 minutes. A fair free offer for all those who want to code on the go, but most of the time have a computer and a locally installed VS Code. You can also host Gitpod yourself on an existing managed Kubernetes environment, for example if you are already a customer of cloud service providers like Amazon, Azure or Google. ConclusionIt will be exciting to see to what extent Microsoft’s own GitHub CodeSpaces will be able to hold its own against this really very manageable opponent once it has been launched en masse. The team from Kiel has at least already set the bar very high, also because it is fantastically documented and structured. A real gain for every tool collection.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Remote","slug":"Remote","permalink":"https://kiko.io/tags/Remote/"}]},{"title":"Discoveries #19 - Visual Helpers","subtitle":null,"series":"Discoveries","date":"2022-07-09","updated":"2022-07-09","path":"post/Discoveries-19-Visual-Helpers/","permalink":"https://kiko.io/post/Discoveries-19-Visual-Helpers/","excerpt":"Colors and images are the visual meat on the boil of any web solution. If you don’t convince the visitor’s eye, they will quickly leave and if users have to work with a visually poor solution, they will be too dissatisfied, no matter how well the algorithms work. Below are a few JavaScript libraries that help to create appealing interfaces. Color ThiefVibrant ColorsColor.jsTinyColorQix colorAlpha PaintletDOM to ImageimagesLoadedGraphery SVGFlickity","keywords":"colors images visual meat boil web solution dont convince visitors eye quickly leave users work visually poor dissatisfied matter algorithms javascript libraries create appealing interfaces color thiefvibrant colorscolorjstinycolorqix coloralpha paintletdom imageimagesloadedgraphery svgflickity","text":"Colors and images are the visual meat on the boil of any web solution. If you don’t convince the visitor’s eye, they will quickly leave and if users have to work with a visually poor solution, they will be too dissatisfied, no matter how well the algorithms work. Below are a few JavaScript libraries that help to create appealing interfaces. Color ThiefVibrant ColorsColor.jsTinyColorQix colorAlpha PaintletDOM to ImageimagesLoadedGraphery SVGFlickity Color Thief by Lokesh Dhakar https://lokeshdhakar.com/projects/color-thief/ Lokesh has developed a JS library which extracts a color palette from any given image. Very useful to adjust the colors of a page for example to the hero image. It works in the client as well as in Node.JS applications. Vibrant Colors by Corbin Crutchley et al https://github.com/Vibrant-Colors/node-vibrant Corbin Crutchley is one of the maintainer of the library Color Vibrant, which extracts the colors from a given image as Color Thief does, but with many more features. It classifies the colors in the extracted palette for using as common shortcuts, it has a WebWorker for avoiding freezing the UI thread and it has converting methods into several color spaces. Stunning work … see the Pen from Konstantin Polunin. Color.js - Let's get serious about color by Lea Verou & Chris Lilley https://colorjs.io/ As Lea Verou says in her blog post on releasing Color.js, there was a lack of color libraries that did the things she (and many others) needed on working with colors. So she teamed up with Chris Lilley, the father of SVG, to create a JS library that covers pretty much everything regarding color coding. I bet Color.js will become a new standard lib for all of us. TinyColor by Brian Grinstead https://github.com/bgrins/TinyColor Brian’s ambitions were certainly not the same as Lea Verou’s, but with TinyColors he has started something, that can be quite helpful on a smaller scale in converting from one color space to another. Qix color by Josh Junon https://github.com/Qix-/color Josh Junon, or ‘Qix’ on Github, provides us a lib with only 496 lines and 10.9 KB, for immutable color conversion and manipulation with support for CSS color strings. For in between… Alpha Paintlet by Dave Rupert https://daverupert.com/2021/10/alpha-paintlet/ The Web API CSS.paintWorklet (see MDN) is an experimental feature in Chromium browsers for extending CSS with JavaScript by writing Worklets. Dave shows us how to do this with his ‘Alpha Paintlet’, which manipulates the alpha channel. DOM to Image by Anatolii Saienko https://github.com/tsayen/dom-to-image Ever wanted to store an arbitary DOM node as an image? With Anatolii’s solution a breeze. Just load the library and call domtoimage.toPng(node). It supports PNG, JPEG and SVG. imagesLoaded by David DeSandro https://imagesloaded.desandro.com/ Sometimes it is important to know when an image was loaded on a website, for example to follow up with further actions. David has a Vanilla script and jQuery solution for this problem and it works with background images too. An important helper … well done. Graphery SVG by -unknown- https://www.graphery.org/svg/ Writing an SVG is not really an amusement. If you are more familiar with JS, you can use Vanilla JS with lots of createElement and setAttribut or the wrapper solution from Graphery, which is chainable and very well documented. Flickity by Evan S https://codepen.io/Skoulix/pen/BRJRPd Last but not least, a very cool hero image solution from Evan. It uses the parallax effect for sliding hero images in the background. Very cool.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Old Sweetheart Rediscovered","subtitle":"OutlookSignature lives on as Go application","date":"2022-06-21","updated":"2022-06-21","path":"post/Old-Sweetheart-Rediscovered/","permalink":"https://kiko.io/post/Old-Sweetheart-Rediscovered/","excerpt":"It seems like ages ago that I wrote a tool called OutlookSignature with Visual Basic 6 and put it on the web on my old German blog zerbit.de. But the WayBackMachine says something different. Started in 2006, I released the last version 1.9 at the beginning of December 2008. Just 14 years ago… The thing was a command line tool, that could be used to automatically generate signatures for Microsoft Outlook in the three formats TXT, RTF and HTML, for example centrally via a Windows login script for the entire organization. No hassle anymore for the users on creating an appropriate mail signature and no more stress for the marketing department in enforcing a uniform appearance. It was based on templates with placeholders for the data and configurable via an INI file. The data could come either from the ActiveDirectory via LDAP or from any database.","keywords":"ages ago wrote tool called outlooksignature visual basic put web german blog zerbitde waybackmachine started released version beginning december years ago… thing command line automatically generate signatures microsoft outlook formats txt rtf html centrally windows login script entire organization hassle anymore users creating mail signature stress marketing department enforcing uniform appearance based templates placeholders data configurable ini file activedirectory ldap database","text":"It seems like ages ago that I wrote a tool called OutlookSignature with Visual Basic 6 and put it on the web on my old German blog zerbit.de. But the WayBackMachine says something different. Started in 2006, I released the last version 1.9 at the beginning of December 2008. Just 14 years ago… The thing was a command line tool, that could be used to automatically generate signatures for Microsoft Outlook in the three formats TXT, RTF and HTML, for example centrally via a Windows login script for the entire organization. No hassle anymore for the users on creating an appropriate mail signature and no more stress for the marketing department in enforcing a uniform appearance. It was based on templates with placeholders for the data and configurable via an INI file. The data could come either from the ActiveDirectory via LDAP or from any database. I had quite a large fan base and even years later I kept getting requests to integrate new features. The problem I faced after releasing the last version 1.9 in 2008 was on the one hand that Microsoft had fundamentally changed the handling of signatures in Outlook and on the other hand that VB6 was no longer really en-vogue, because everybody (and me too) was switching to VB.NET or C# and that would have meant a complete re-write for me. But there were already commercial alternatives to OutlookSignature at that time and I had turned to other projects, but it was available into 2012 until I decided to close my personal blog. However, to this day there are one or two websites where you can download or find information about it. OutlookSignature was always freeware, but never Open Source, because of the fact, that I used some code, that did not come from me and was not approved for publication. Accidental Discovery of a Go-based ImplementationI own the domain zerbit.de until today and the other day I was looking on the web for some references to it and stumbled across a GitHub page with the title An open source reimplementation of Kristof Zerbe’s (ZerbIT) “OutlookSignature” … What The Heck!? I don’t know who the user ‘foobar0815’ is, but he/she is definitely German and had the patience to rebuild my tool in the modern language Go in 2019. GoSignature uses the orginal INI configuation file, with the field mappings and all the other stuff, but the database connection feature. It works only with LDAP, which is quite enough today. I have no idea about Go, but I will certainly look into it a bit in the near future and find out if this thing works. If you have already tried GoSignature or even if you are the author, please contact me. Would be fun to talk about it… :)","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Mail","slug":"Mail","permalink":"https://kiko.io/tags/Mail/"},{"name":"Office","slug":"Office","permalink":"https://kiko.io/tags/Office/"}]},{"title":"Simplest Console File Logger","subtitle":"How to implement your own logger with just a StringBuilder in C#","date":"2022-06-19","updated":"2022-06-19","path":"post/Simplest-Console-File-Logger/","permalink":"https://kiko.io/post/Simplest-Console-File-Logger/","excerpt":"When you need to do a task in IT and don’t need a fancy user interface, you usually turn to a console application or develop one. But no UI means not, that you don’t want to get some information about the status of the running program. The means of choice in C#/.NET is then the output of certain values in the console via console.WriteLine(). But often these applications are supposed to run in the background or hidden, so that crashes or errors are not immediately noticeable if you don’t have at least a rudimentary logging built in. Such logging is also useful for the later evaluation of program runs. For this reason pretty much all programs log in some way. Currently there an incredible number of logging frameworks available, that make it to the news every now and then, like Log4Shell for Java. The best known in the .NET area is Microsoft’s own ILogger (Microsoft.Extensions.Logging), log4net, NLog and Serilog. What they all have in common is, that they are highly flexible in terms of configuration, usage and storage of the logs. On the other hand, they are fat beasts, you have to learn to handle first and you have to deliver with the program always. The latter does not apply to the ILogger, but Microsoft has been known to complicate things so unnecessarily that it is almost no fun anymore. Many times this overload is simply not necessary, when you just need a log file for each run of your console application. Let me show you, how to achieve this with a dead simple StringBuilder…","keywords":"task dont fancy user interface turn console application develop ui means information status running program choice c#/net output values consolewriteline applications supposed run background hidden crashes errors immediately noticeable rudimentary logging built evaluation runs reason pretty programs log incredible number frameworks make news log4shell java net area microsofts ilogger microsoftextensionslogging log4net nlog serilog common highly flexible terms configuration usage storage logs hand fat beasts learn handle deliver apply microsoft complicate things unnecessarily fun anymore times overload simply file show achieve dead simple stringbuilder…","text":"When you need to do a task in IT and don’t need a fancy user interface, you usually turn to a console application or develop one. But no UI means not, that you don’t want to get some information about the status of the running program. The means of choice in C#/.NET is then the output of certain values in the console via console.WriteLine(). But often these applications are supposed to run in the background or hidden, so that crashes or errors are not immediately noticeable if you don’t have at least a rudimentary logging built in. Such logging is also useful for the later evaluation of program runs. For this reason pretty much all programs log in some way. Currently there an incredible number of logging frameworks available, that make it to the news every now and then, like Log4Shell for Java. The best known in the .NET area is Microsoft’s own ILogger (Microsoft.Extensions.Logging), log4net, NLog and Serilog. What they all have in common is, that they are highly flexible in terms of configuration, usage and storage of the logs. On the other hand, they are fat beasts, you have to learn to handle first and you have to deliver with the program always. The latter does not apply to the ILogger, but Microsoft has been known to complicate things so unnecessarily that it is almost no fun anymore. Many times this overload is simply not necessary, when you just need a log file for each run of your console application. Let me show you, how to achieve this with a dead simple StringBuilder… Lets start in the Program.cs of a new .NET Core 6 console application. As you may know, in this version MS has changed the entry point static void Main(string[] args), which has existed for what feels like centuries, to the so-called Top-Level Statements, which means nothing else, that the entry point is now automatically generated … and what most old-school developer think sucks, because you have little control over it anymore. But for this approach we need a global variable and therefore I’ve recreated Main like this: Program.cs123456789101112// TOP-LEVEL STATEMENTSMain.Start(args);// MAIN MODULEstatic public class Main { static public void Start(string[] args) { //... }} Preparation of a small senseless console applicationLet’s implement a little bit of the ‘features‘ first, like a new class called DoSomething that does the actual job: DoSomething.cs123456789101112public class DoSomething{ public void Work(string param1) { //... doing heavy work with param1 } public void MoreWork(string param2) { //... doing more work with param2 }} Now let’s process the command line arguments for running the app called SimplestConsoleFileLogger like this… SimplestConsoleFileLogger.exe param1:this param2:that First we create a new class for holding the parameters… Parameters.cs12345public class Parameter{ public string Param1 = ""; public string Param2 = "";} … and then we populate a new instance of it with the values from the console arguments and run the worker class: Program.cs1234567891011121314151617181920212223static public class Main { static Parameter PARAMETER = new Parameter(); static public void Start(string[] args) { //Process Arguments foreach (string arg in args) { var a = arg.Split(':'); switch (a[0].ToUpper()) { case "PARAM1": PARAMETER.Param1 = a[1]; break; case "PARAM2": PARAMETER.Param1 = a[1]; break; default: break; } } DoSomething ds = new DoSomething(); ds.Work(PARAMETER.Param1); ds.MoreWork(PARAMETER.Param2); }} The LoggingNow we have our pretty senseless app and we want to log the jobs of the worker class. First, we have to implement a global variable for holding the log messages and secondly a small method to simplify the handling of the log: Program.cs12345678910111213static public class Main { ... static StringBuilder LOGDATA = new StringBuilder(); public static void Log(params string[] args) { var msg = string.Join(" ", args).TrimEnd(); LOGDATA.AppendLine(msg); } } The Log methods gets an infinite number of arguments (ParamArray) to assemble a message to log. This we can use now in our worker class: DoSomething.cs1234567891011121314public class DoSomething{ public void Work(string param1) { //... doing heavy work with param1 Main.Log("Heavy work done with result", "SUCCESS"); } public void MoreWork(string param2) { //... doing more work with param2 Main.Log("More work done with result", "SUCCESS"); }} You can use Main.Log(...) wherever you want, because its globally available. Writing the log fileLast, but not least, we have to persist the log messages into a file. This should done just before the application ends. For writing the log, we use a StreamWriter object, which accepts our StringBuilder, where the messages are stored in seperate lines. By checking the length of the StringBuilder LOGDATA, we ensure that the log file is only written, when there is something to write. Program.cs123456789101112131415161718192021static public class Main { static public void Start(string[] args) { ... HandleEnd(); } static void HandleEnd() { if (LOGDATA.Length > 0) { string logFile = $"{DateTime.Now.ToString("u").Replace(":","-")}.log"; string logFilePath = Path.Combine(Environment.CurrentDirectory, logFile); StreamWriter logStream = new StreamWriter(logFilePath, true, Encoding.UTF8); logStream.Write(LOGDATA); logStream.Close(); } }} That’s it. As you see, the important parts are the globally available StringBuilder and the persistance of the messages in a file at the end. With this you can customize the messages as fancy as you want and you don’t need one of the logging beasts. Download the files via Gist","categories":[{"name":".NET","slug":"NET","permalink":"https://kiko.io/categories/NET/"}],"tags":[{"name":"Visual Studio","slug":"Visual-Studio","permalink":"https://kiko.io/tags/Visual-Studio/"},{"name":"Logging","slug":"Logging","permalink":"https://kiko.io/tags/Logging/"},{"name":"C#","slug":"C","permalink":"https://kiko.io/tags/C/"}]},{"title":"Thanks Dropbox, but I'm off","subtitle":"How to do homework or say goodbye to the market","date":"2022-05-13","updated":"2022-05-13","path":"post/Thanks-Dropbox-but-I-m-off/","permalink":"https://kiko.io/post/Thanks-Dropbox-but-I-m-off/","excerpt":"I’m a customer of Dropbox many, many years, a paying customer, and was always happy about the service, as it is fast and easy to use. No problems … until now! Be aware … this will be a rant :/ But first a step back: Nowadays it is normal to work with different classes of devices: stationary PCs, different types of laptops, tablets and also smartphones. You start writing a text on your laptop at home in the garden, have to interrupt it because of an appointment and continue writing on your tablet on your way, only to finish it at home on your PC, because it has started to rain. All of these used devices to write the text may have a different hard- and software configuration, but today we have synchronisation services like Dropbox, OneDrive, GoogleDrive and man others, which ensures that the same version of the text is available at all times at all devices. To me it is obvious that software manufacturers cannot support every operating system, but I can expect that if they support a particular OS, they will do so on any hardware that the OS manufacturer also supports!","keywords":"im customer dropbox years paying happy service fast easy problems … aware rant / step back nowadays normal work classes devices stationary pcs types laptops tablets smartphones start writing text laptop home garden interrupt appointment continue tablet finish pc started rain write hard- software configuration today synchronisation services onedrive googledrive man ensures version times obvious manufacturers support operating system expect os hardware manufacturer supports","text":"I’m a customer of Dropbox many, many years, a paying customer, and was always happy about the service, as it is fast and easy to use. No problems … until now! Be aware … this will be a rant :/ But first a step back: Nowadays it is normal to work with different classes of devices: stationary PCs, different types of laptops, tablets and also smartphones. You start writing a text on your laptop at home in the garden, have to interrupt it because of an appointment and continue writing on your tablet on your way, only to finish it at home on your PC, because it has started to rain. All of these used devices to write the text may have a different hard- and software configuration, but today we have synchronisation services like Dropbox, OneDrive, GoogleDrive and man others, which ensures that the same version of the text is available at all times at all devices. To me it is obvious that software manufacturers cannot support every operating system, but I can expect that if they support a particular OS, they will do so on any hardware that the OS manufacturer also supports! What has happened…I have similar hardware as described above, but with a gap: a Windows tablet. I have one with Android and manage my synchronization with a tool called DropSync, but due to limited disc space I can’t sync bigger projects with thousands of files, like this blog. Whenever I want to continue writing an article on the road, I download the corresponding MD file and then upload it again afterwards. A bit cumbersome. Since the first appearance of Microsoft’s Surface several years ago, I thought this type of hardware, which includes the same OS I’m using all the time - Windows - could be close my device gap and a couple of days ago, I put a bunch of Euros in the hand of a local dealer to finally close it with a Surface Pro X. 256GB SSD, 16GB RAM and the size of a sheet of paper should be good to work on stuff while on the road. As with any new piece of hardware, I began to install my setup, essential tools that I use very often. This included Dropbox, of course … with an unexpected result. While all the tools installed without a murmur, Dropbox bitched by saying that my device is not compatible with this version of the Dropbox client and that I should install the Dropbox S Mode app via the Windows Store instead. Yes sure, a Surface has an ARM processor instead of an x32/x64 from Intel or AMD, but what’s the problem? Every single tool in my list works with Windows 10 under ARM64, at least in 32-bit emulation, but not Dropbox? To say it loud and clear: the UWP app “Dropbox S Mode” is a disgrace! Not only does it use a technology that Microsoft is just saying goodbye to, no, it also offers less functionality than the actual Dropbox website. Folders cannot be downloaded as a whole, but only individual files, and a synchronization of some kind has been completely omitted. The thing is really just dirt. This kind of software is for grannies who already have a hard time holding a mouse, but not for power users, with several gigabytes of data! I don’t let something like that sit on me so easily, but tried to find a trick via some research how to get the Dropbox client to work under ARM after all. And on Dropbox’s own forum alone, I found a bunch of posts on the topic: Is Windows 10 on ARM going to be compatible with the Dropbox desktop app? Can I have the desktop app installed on my Surface Pro X? Dropbox client for Windows 11 ARM? Can’t install the desktop app on a new PC The oldest post I have found here was from 2019. Dropbox, you are not able to port your code to ARM within at least 3 years and the only answer you have for your paying users is buy a regular computer. Are you nuts? Are you begging to go under or be bought by one of the big fish due to management incompetence? New Dropbox features like Paper, Capture or Replay are nice ones, but whenever a company neglects its base, the reason why became big, it is close to the abyss. Best examples are Lego and Marvel. A New StartI was aware of OneDrive (formerly SkyDrive) since it was launched 2007, as Microsoft tried to counter the success of Dropbox, but it had its pitfalls and wasn’t running so fluffy as the original. 15 years later, you can hardly resist the sync software from Redmont, because it is always included in everything that has Windows on it, but it has also become decisively better and more stable. The ARM disaster of Dropbox has now moved me to switch to OneDrive. I’m writing this article on my Surface tablet right now, and whenever I press CTRL-S, OneDrive syncs the MD file to my other machines. At the end of the day, we’re all about one thing: Convenience, and if you don’t meet that standard (or abuse it), you die. Good Luck Dropbox …","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Rant","slug":"Rant","permalink":"https://kiko.io/tags/Rant/"}]},{"title":"Discoveries #18 - JS UI","subtitle":null,"series":"Discoveries","date":"2022-05-10","updated":"2022-05-10","path":"post/Discoveries-18-JS-UI/","permalink":"https://kiko.io/post/Discoveries-18-JS-UI/","excerpt":"This months Discoveries it’s all about Web UI driven by JavaScript. I have found some really cool components in the net you can use in your project right away to give your users a bit more food for the eye. Congrats to the developers for their amazing work. Scroll-Linked Animations With the Web Animations API (WAAPI) and ScrollTimelineSlider ScrollAdd-to-Calendar ButtonResponsive Clock UiSwiffy SliderFloating UI8 CSS & JavaScript Snippets for Creating Cool Card UI Hover EffectsHow To Build An Expandable Accessible GalleryOmicron drag&dropLetMeScroll","keywords":"months discoveries web ui driven javascript found cool components net project give users bit food eye congrats developers amazing work scroll-linked animations api waapi scrolltimelineslider scrolladd-to-calendar buttonresponsive clock uiswiffy sliderfloating ui8 css snippets creating card hover effectshow build expandable accessible galleryomicron drag&dropletmescroll","text":"This months Discoveries it’s all about Web UI driven by JavaScript. I have found some really cool components in the net you can use in your project right away to give your users a bit more food for the eye. Congrats to the developers for their amazing work. Scroll-Linked Animations With the Web Animations API (WAAPI) and ScrollTimelineSlider ScrollAdd-to-Calendar ButtonResponsive Clock UiSwiffy SliderFloating UI8 CSS & JavaScript Snippets for Creating Cool Card UI Hover EffectsHow To Build An Expandable Accessible GalleryOmicron drag&dropLetMeScroll Scroll-Linked Animations With the Web Animations API (WAAPI) and ScrollTimeline by Bramus Van Damme https://css-tricks.com/scroll-linked-animations-with-the-web-animations-api-waapi-and-scrolltimeline/ Bramus shows us how to implement a reading progress bar with the new ScrollTimeline feature coming up within the next versions of Chrome and other browsers, including a polyfill from Robert Flack. As you read this … look at the orange bar at the top. Slider Scroll by Edixon Piña https://sliderscroll.netlify.app/ Ever wanted to create a web application consisting of individual slides? Then, this is for you. Edixon has to (nearly) perfect and clean solution for this on GitHub. Add-to-Calendar Button by Jens Kuerschner https://github.com/jekuer/add-to-calendar-button Jens offers a nice solution for everybody who wants their users to add an events to their calendar within two clicks. It is based on inline JSON-snippets and has several features like dynmamic days, timezone offsets, schema.org support and not to forget, a pleasing UI. Responsive Clock Ui by Bedimcode https://github.com/bedimcode/responsive-clock-ui Marlon, a web designer from Peru, who is known on the web as Bedimcode, has created a stunning CSS- and JS-driven analog clock, with a light and a dark theme. Maybe a bit useless for a website, but wonderfully crafted and nice to look at. You can find a demo here. Swiffy Slider by DynamicWeb https://swiffyslider.com/ Sliders and carousels are almost everywhere and there are many implementations out there, but this one from the team of DynamicWeb is a bit outstanding because of its features and application possibilities. Well done … espacially the well documentation. No questions left. Floating UI by Unknown https://floating-ui.com/ Whoever has created this solution, is hardly anything to add to their claim: “Floating UI is a low-level toolkit to create floating elements. Tooltips, popovers, dropdowns, menus, and more.”. and it is hardly anything to add to their feature list. Whenever something needs to float … use this library. 8 CSS & JavaScript Snippets for Creating Cool Card UI Hover Effects by Eric Karkovack https://speckyboy.com/css-javascript-card-ui-hover-effects/ Cards are a good way to bring order in a UI. How to make such cards exciting shows Eric with his collection of stunning card designs. Personally I’m in love with the ‘Profile Card Hover Effect’… How To Build An Expandable Accessible Gallery by Silvestar Bistrović https://www.smashingmagazine.com/2021/10/build-expandable-accessible-gallery/ Photo in galleries or other grid based collections should have the possibility to expand an item in order to see either the full photo or some details of the selected item. To avoid calling a detail page or something similar, Silvestar has developed a method to display this animated in fullscreen mode. Can’t wait to find a problem to check this out. Omicron drag&drop by Franek Boehlke https://github.com/mcFrax/omicron-dnd What sounds like a variant of the Corona virus is a fast JavaScript drag-and-drop library for desktop and mobile browsers, made by Franek Boehlke. And this is no coincidence, but rather a pandemic project. It is lean, but has all basic features to nudge things around on your web app. LetMeScroll by Bruno Vieira https://bmsvieira.github.io/letmescroll.js/demo/index.html Quite a while ago a have implemented a custom scrollbar on my own in a Web App and it wasn’t so funny as I thought. Lot of things to care about. If Bruno had already finished his project at that time, I could have saved myself the work. He brings a native scroll behaviour for desktop and mobile, easy customization and multiple callbacks in 465 lines of vanilla JS code. Great job, Bruno.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Discoveries #17 - CSS","subtitle":null,"series":"Discoveries","date":"2022-03-11","updated":"2022-03-11","path":"post/Discoveries-17-CSS/","permalink":"https://kiko.io/post/Discoveries-17-CSS/","excerpt":"Easing Functions Cheat SheetA New Way To Reduce Font Loading Impact: CSS Font DescriptorsContainer Queries in Web ComponentsMulti Color Text With CSSConditional Border Radius In CSSIcons in Pure CSSparallax.cssDefensive CSSHow to Create a CSS-Only Loader Using One ElementGetting Your head Straight: A New CSS Performance Diagnostics Snippet","keywords":"easing functions cheat sheeta reduce font loading impact css descriptorscontainer queries web componentsmulti color text cssconditional border radius cssicons pure cssparallaxcssdefensive csshow create css-only loader elementgetting head straight performance diagnostics snippet","text":"Easing Functions Cheat SheetA New Way To Reduce Font Loading Impact: CSS Font DescriptorsContainer Queries in Web ComponentsMulti Color Text With CSSConditional Border Radius In CSSIcons in Pure CSSparallax.cssDefensive CSSHow to Create a CSS-Only Loader Using One ElementGetting Your head Straight: A New CSS Performance Diagnostics Snippet Easing Functions Cheat Sheet by Andrey Sitnik & Ivan Solovev https://github.com/ai/easings.net Ever wanted to implement an easing function in CSS and dont know how? Andrey and Ivan provide a really useful web project via Github, with the code of 30 easing function, including samples and the math functions behind. Download or fork and run locally or simply use this gitpod Link. A New Way To Reduce Font Loading Impact: CSS Font Descriptors by Barry Pollard https://www.smashingmagazine.com/2021/05/reduce-font-loading-impact-css-descriptors/ Barry gives an insight into the modern use of web fonts, hints on how to use font-display and an outlook on how everything could be better in the future. Container Queries in Web Components by Max Böck https://mxb.dev/blog/container-queries-web-components/ Container Queries are one of the most anticipated new features in CSS and Max gives an insight into the possibilities, including a demo. Multi Color Text With CSS by Shireen Taj https://codepen.io/TajShireen/full/YzZmbep UI developer Shireen has made a wonderful sample on CodePen on how to show text in multile colors using linear gradient backgrounds. Conditional Border Radius In CSS by Ahmad Shaheed https://ishadeed.com/article/conditional-border-radius/ CSS expert Ahmad shows us a smart way to provide different border radius regarding the size of an object in the viewport, he has learned from the facebook developers. Icons in Pure CSS by Anthony Fu https://antfu.me/posts/icons-in-pure-css Anthony has found a clever way on combining the icon framework iconify with CSS, to save the use of icon fonts and deliver pure SVG instead. parallax.css by Matyanson https://github.com/Matyanson/parallax.css/ Github user ‘Matyanson’ has written a CSS package for using parallax effects easily on every website. Defensive CSS by Ahmad Shaheed https://ishadeed.com/article/defensive-css/ In order to prevent pittfalls, you have to use CSS wisely. Ahmad show us some techniques on how to write defensive and thus protected code. How to Create a CSS-Only Loader Using One Element by Temani Afif https://www.freecodecamp.org/news/how-to-create-a-css-only-loader Loaders are everywhere and many of them are based on images or SVG. But this does not have to be, as Temani finds. CSS is perfectly sufficient for this. Getting Your head Straight: A New CSS Performance Diagnostics Snippet by Vitaly Friedman https://www.smashingmagazine.com/2021/09/css-head-tag/ Chrome Debugger has tons of possibilities to ensure to find performance bottlenecks or other discrepancies. Vitaly brings us closer a little snippet that Harry Roberts wrote, which exposes potential performance issues in your page’s <head> tag: ct.css.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Checker Plus - Gmail in better...","subtitle":"Few Chrome browser extensions are as close to user needs as Checker Plus for Google's Gmail, Calendar and Drive services.","series":"Golem","date":"2022-02-12","updated":"2022-02-12","path":"post/Checker-Plus-Gmail-in-better/","permalink":"https://kiko.io/post/Checker-Plus-Gmail-in-better/","excerpt":"Those who have a Google account usually also use the web-based mailer Gmail, often Google Calendar for appointments and perhaps Google Drive for storing files in the cloud. The US internet company makes it very easy for users to use its web-based services. The entry barriers are low and the interfaces are tidy and intuitive. If you use the in-house Chrome browser, you can also be informed about incoming mails or upcoming appointments. One click and the browser opens with Gmail or Google Calendar … But it is not always necessary or desirable to load one of the Google tools completely in a new browser window, when you may already have two or three dozen tabs open in several windows and are in danger of losing track of them. That’s what Montreal-based software developer Jason Savard seems to have thought ten years ago. At that time, he began writing a Chrome extension that would report a newly received Gmail by means of a small icon in the extension bar of the browser and display it directly in a separate pop-up window by clicking on it. The result was Checker Plus for Gmail.","keywords":"google account web-based mailer gmail calendar appointments drive storing files cloud internet company makes easy users services entry barriers low interfaces tidy intuitive in-house chrome browser informed incoming mails upcoming click opens … desirable load tools completely window dozen tabs open windows danger losing track montreal-based software developer jason savard thought ten years ago time began writing extension report newly received means small icon bar display directly separate pop-up clicking result checker","text":"Those who have a Google account usually also use the web-based mailer Gmail, often Google Calendar for appointments and perhaps Google Drive for storing files in the cloud. The US internet company makes it very easy for users to use its web-based services. The entry barriers are low and the interfaces are tidy and intuitive. If you use the in-house Chrome browser, you can also be informed about incoming mails or upcoming appointments. One click and the browser opens with Gmail or Google Calendar … But it is not always necessary or desirable to load one of the Google tools completely in a new browser window, when you may already have two or three dozen tabs open in several windows and are in danger of losing track of them. That’s what Montreal-based software developer Jason Savard seems to have thought ten years ago. At that time, he began writing a Chrome extension that would report a newly received Gmail by means of a small icon in the extension bar of the browser and display it directly in a separate pop-up window by clicking on it. The result was Checker Plus for Gmail. In addition to the extension for Gmail, Savard offers others for Calendar and Drive and all of them have one thing in common: You can do the most important functions of the services directly in the popup that opens: Reply to mail, write new mail, create appointment with reminder, upload file, get file link to share - Everything works without having to load the services’ interfaces in a browser window. For the author and probably some of the almost 2 million users, the Checker Plus extensions are the kind of tools you can’t imagine working without them - that’s how quickly you get used to the functionality and adapt your own workflow to it. Checker Plus for Gmail: Impressive range of functionsThe Checker Plus tool for the web mailer Gmail places an icon in the extension bar that displays the number of unread mails by means of an indicator. Since it is possible to configure several Gmail accounts in the extension, all unread mails are summarised in the number. If you click on the icon, a pop-up opens with a clear list of all accounts and the unread mails. The mails are displayed in compact mode, i.e. in addition to the sender and the subject, you also see an excerpt from the mail text. If you now click on a mail, it is displayed in full in the popout and also takes any mail thread into account. The icon buttons Archive, Mark Spam, Delete, Mark Read and Mark, which are displayed when hovering over a mail, represent the most frequently used actions and usually make it unnecessary to use the Open in Gmail button. Massive Setting OptionsOver the years and with each new version (currently v22.9), countless options have been added, so that Savard had to divide them into nine groups, including the start page: Start Page with Quick Access General Notifications Do not disturb Button Accounts/Labels Skins and Themes Keys Voice Input It would be foolish, to go into all the settings in this article, but a few deserve an extra portion of attention, such as those under Accounts/Labels. Here, users can decide whether the extension should determine the available Gmail accounts itself based on the login to Gmail saved in the browser or whether they should specify them manually. In both cases, it is then possible to select only certain labels for the notifications or to leave it at the standard inbox. This is very usefull if you have set up a system of automatic distribution to subfolders (labels). Since the extensions also control the interactive desktop notifications, it is useful to be able to define periods of time under Do Not Disturb, when this is not to happen. In addition to the option of using dates from the Google Calendar, you can also define weekly schedules for this purpose. Checker Plus for Google Calendar: Better than the originalThe icon of the Chrome extension for Google Calendar visually warns of upcoming appointments and displays those for the next two days in the tooltip. If you sit at your computer all day and work in the Chrome browser, you can hardly talk your way out of missing an appointment, especially since the extension also takes over warnings via the Windows notification system. The pop-up’s interface is similar to the original at calendar.google.com, merely placing the sidebar under a hamburger menu to save space. It replicates the basic functionality of the original and there is little reason to visit the original Google website when using it. But it also does a few things better than Google. For example, the pop-up offers the same view options as day, month, week, year and appointment overview, but calls the latter a list and adds an overview as known from the Calendar Android app. In addition, you can define your own view in the settings, for example, if you want to keep an eye on the next three days or six weeks. The current version v26.1.1 of Checker Plus for Google Calendar also has a lot of setting options, such as displaying the week numbers or marking the weekends. Particularly noteworthy here are the settings for notifications, which can be fine-tuned very well across multiple accounts and calendars. Checker Plus for Google Drive: Fewer functionsThe pop-up of the Drive extension is basically a copy of the Google interface at drive.google.com. It is limited to the list form and does without previews of the files, but also without advertisements, such as “Buy storage space”. It also has nowhere near as many options in the context menu of a folder or file as the original, but this can generally be gotten over because the most important options such as download, copy link, rename and remove are available. Additional functions against donationFew Chrome extensions are as close to the needs of users as the Checker Plus extensions. The functional scope of the Gmail extension in particular is simply impressive and leaves hardly anything to be desired. Only when it comes to localising the source code in currently more than 30 languages, Savard could take a little more care. Sometimes a little English gets mixed into otherwise German interfaces, especially in the settings. But any developer who has ever tried to offer his tool in so many languages will forgive him, because he knows the amount of work involved. Unfortunately, the settings area Skins and Themes is everything but generally understandable and no comparison to the otherwise good UX of the extensions. If you want to make the pop-up pretty with background images and other colours, you have to read up and understand Jason’s concept of individualising separate elements. Since Jason Savard now works full time on his extensions, he is dependent on income to be generated from them. He has declared some settings, which he says he worked on for a long time, as “additional functions” that you get if you donate an amount of money to him once or monthly. A fair concept, especially since he guarantees daily updates and even a response time of five minutes in case of problems. Well ;) But it’s nice to see a developer who puts so much heart and soul into his work.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"},{"name":"Mail","slug":"Mail","permalink":"https://kiko.io/tags/Mail/"}]},{"title":"Discoveries #16 - JavaScript","subtitle":null,"series":"Discoveries","date":"2022-01-29","updated":"2022-01-29","path":"post/Discoveries-16-JavaScript/","permalink":"https://kiko.io/post/Discoveries-16-JavaScript/","excerpt":"In the first discoveries of 2022 I would like to offer you some interesting links to JavaScript articles that have general language features as their topic or extend them in a clever way. The language (respectively the ECMA standard behind it) is growing from year to year and it is exiting to see how it is expanding. Modern Javascript: Everything you missed over the last 10 yearsHow can I define an enum in JavaScript?'export default thing' is different to 'export { thing as default }'Dynamic, Conditional Imports10 Client-side Storage Options and When to Use ThemAn Intro to JavaScript ProxyThe Observer Pattern in JavaScript - The Key to a Reactive BehaviorWhy JavaScript Developers Should Prefer Axios Over FetchToolkit for managing multiple promisesPromisify an entire object or class instance","keywords":"discoveries offer interesting links javascript articles general language features topic extend clever ecma standard growing year exiting expanding modern missed yearshow define enum javascript'export default thing export { }'dynamic conditional imports10 client-side storage options theman intro proxythe observer pattern - key reactive behaviorwhy developers prefer axios fetchtoolkit managing multiple promisespromisify entire object class instance","text":"In the first discoveries of 2022 I would like to offer you some interesting links to JavaScript articles that have general language features as their topic or extend them in a clever way. The language (respectively the ECMA standard behind it) is growing from year to year and it is exiting to see how it is expanding. Modern Javascript: Everything you missed over the last 10 yearsHow can I define an enum in JavaScript?'export default thing' is different to 'export { thing as default }'Dynamic, Conditional Imports10 Client-side Storage Options and When to Use ThemAn Intro to JavaScript ProxyThe Observer Pattern in JavaScript - The Key to a Reactive BehaviorWhy JavaScript Developers Should Prefer Axios Over FetchToolkit for managing multiple promisesPromisify an entire object or class instance Modern Javascript: Everything you missed over the last 10 years by Sandro Turriate https://turriate.com/articles/modern-javascript-everything-you-missed-over-10-years Sandra has written this post about the language features of ECMA Script 2020 a couple of months ago as a kind of CheatSheet, with runnable examples and a lot of useful background knowledge. How can I define an enum in JavaScript? by Angelos Chalaris https://www.30secondsofcode.org/articles/s/javascript-enum Angelos describes is this post, two different ways to define enums in JavaScript, as you might know them other languages. 'export default thing' is different to 'export { thing as default }' by Jake Archibald https://jakearchibald.com/2021/export-default-thing-vs-thing-as-default/ IMPORT and EXPORT are a fine way to separate code, but you need to know and keep in mind a few things, as Jake shows us here. Dynamic, Conditional Imports by Chris Coyier https://css-tricks.com/dynamic-conditional-imports/ When you separate code in different ES modules, you may come to the point where you want import a module depending on a specific condition. Chris show us here, how to deal with this easily. 10 Client-side Storage Options and When to Use Them by Craig Buckler https://www.sitepoint.com/client-side-storage-options-comparison/ Especially if you are writing a web app, you need to consider what storage options you have, to provide the user an optimal user experience. Craig list them all in his post and goes into the specific characteristics and possible uses. An Intro to JavaScript Proxy by Travis Almand https://css-tricks.com/an-intro-to-javascript-proxy/ JavaScript provides a PROXY, which enables you to intercept and redefine fundamental operations for an object. Is it not that common to use it, but it has great advantages, as Travis show us. The Observer Pattern in JavaScript - The Key to a Reactive Behavior by Fernando Doglio https://blog.bitsrc.io/the-observer-pattern-in-javascript-the-key-to-a-reactive-behavior-f28236e50e10 Sometimes it is necessary to decouple functionality in JS, in order to write cleaner code or to increase complexity. Fernando shows us how to implement our own observers, with subscribing, notifying and all that stuff. Why JavaScript Developers Should Prefer Axios Over Fetch by Sabesan Sathananthan https://betterprogramming.pub/why-javascript-developers-should-prefer-axios-over-fetch-294b28a96e2c FETCH is the method defined in the ECMA standard to get data from remote servers, but in the meanwhile the library Axios has become almost a de-facto standard in the industry and Sabesan tells why. Toolkit for managing multiple promises by Anthony Fu https://github.com/antfu/p This tiny library makes it easier to deal with multiple promises as it PROMISE.ALL does. Remarkable. Promisify an entire object or class instance by Gar https://github.com/wraithgar/gar-promisify Mixing async with non-async code may have some pitfalls, you can avoid by using Gar’s tiny library.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"CSS Columns and Drop Shadow","subtitle":"How to fix Chromes bug on showing shadows in a column (masonry) layout","date":"2022-01-15","updated":"2022-01-15","path":"post/CSS-Columns-and-Drop-Shadow/","permalink":"https://kiko.io/post/CSS-Columns-and-Drop-Shadow/","excerpt":"As of today, there is no true Masonry Layout technique in web development that can be implemented exclusively with CSS and is not based on JavaScript. True in the meaning, that the reading direction should be from left to right and not in the form of columns from top to bottom. For the latter, also called Fake Masonry, there are even two implementation options in CSS: Columns or Flex, whereas the Columns variant is the much simpler one. Lets say you have a list of boxes you want to show in a grid-like list, but the height of every box is defined by its content, which results in different heights. Here is an example with a base64 encoded 1px image with an individually defined height: 123456789101112131415<div class="wrapper"> <div class="item"> Box 1 <img style="height:53px" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /> </div> <div class="item"> Box 2 <img style="height:50px" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /> </div> <div class="item"> Box 3 <img style="height:33px" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /> </div> <!-- ... 6 more items with different heights --></div> By applying column-count and column-gap to the wrapper and a margin-bottom with the same value as the gap to each item, you will achieve this: The order of the boxes is from top to bottom and then from left to right … Fake Masonry, but is works as expected. Here is the most important CSS:","keywords":"today true masonry layout technique web development implemented exclusively css based javascript meaning reading direction left form columns top bottom called fake implementation options flex variant simpler lets list boxes show grid-like height box defined content results heights base64 encoded 1px image individually 123456789101112131415<div class="wrapper"> <div class="item"> <img style="height53px" src="dataimage/gifbase64r0lgodlhaqabaiaaap///waaach5baeaaaaalaaaaaabaaeaaaicraeaow==" /> </div> style="height50px" style="height33px" <-- items --></div> applying column-count column-gap wrapper margin-bottom gap item achieve order … works expected important","text":"As of today, there is no true Masonry Layout technique in web development that can be implemented exclusively with CSS and is not based on JavaScript. True in the meaning, that the reading direction should be from left to right and not in the form of columns from top to bottom. For the latter, also called Fake Masonry, there are even two implementation options in CSS: Columns or Flex, whereas the Columns variant is the much simpler one. Lets say you have a list of boxes you want to show in a grid-like list, but the height of every box is defined by its content, which results in different heights. Here is an example with a base64 encoded 1px image with an individually defined height: 123456789101112131415<div class="wrapper"> <div class="item"> Box 1 <img style="height:53px" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /> </div> <div class="item"> Box 2 <img style="height:50px" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /> </div> <div class="item"> Box 3 <img style="height:33px" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /> </div> <!-- ... 6 more items with different heights --></div> By applying column-count and column-gap to the wrapper and a margin-bottom with the same value as the gap to each item, you will achieve this: The order of the boxes is from top to bottom and then from left to right … Fake Masonry, but is works as expected. Here is the most important CSS: 123456789101112131415161718192021222324252627:root { --gap: 20px; --gap-half: calc(var(--gap) / 2);}.wrapper { /* LAYOUT STYLES */ width: max-content; margin: 0; padding: var(--gap-half); /* COLUMN STYLES */ column-count: 3; column-gap: var(--gap);}.item { /* BASIC STYLES */ background-color: #fffaf5; /* LAYOUT STYLES */ width: 200px; margin-bottom: var(--gap); padding: 15px; overflow: auto; /* COLUMN STYLES */ break-inside: avoid-column;} See the pen for the complete HTML and CSS: Drop Shadow and the Chromium BugTo make the list visually a little bit more interesting, we now add a shadow, which is half as thick as the gap, to the boxes: 12345678910:root { --gap: 20px; --gap-half: calc(var(--gap) / 2); --color-border: hsl(0 0% 90%);}.item { /* ... more styles */ border: 1px var(--color-border) solid; box-shadow: 1px 2px var(--gap-half) 5px var(--color-border);} In case you work with a Chromium based browser (Version 97.x as of today), you will be confronted with a bug. The “break” from one column to the next doesn’t respect the full 10px high shadow of the next item. It breaks too late. You will get something like this: I added a red shadow on hover, to make the bug more obvious. See following pen to inspect the CSS. In case you use Firefox (Version 96.x as of today), you won’t see the bug, because Mozilla did it right. WorkaroundStep 1In order to achieve a proper result, we have to hack the HTML and the CSS a bit. First of all we have to wrap the content of an item with a new element called ìtem-inner: 123456<div class="item"> <div class="item-inner"> Box 1 <img style="height:53px" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /> </div></div> Step 2Next, we move the width, the border and the box-shadow from the item itself to the new inner element. Important here is, to set the background of the item element to transparent in order to prevent interfering with the shadow. 123456789101112131415161718192021:root { --gap: 20px; --gap-half: calc(var(--gap) / 2); --color-border: hsl(0 0% 90%);}.item { /* BASIC STYLES */ background-color: transparent;}.item-inner { /* BASIC STYLES */ background-color: #fffaf5; border: 1px var(--color-border) solid; box-shadow: 1px 2px var(--gap-half) 5px var(--color-border); transition: all ease-out 0.3s; /* LAYOUT STYLES */ width: 200px; padding: 15px; overflow: hidden;} Step 3Now we have to set the wrapper‘s gap to 0, because we will implement the spacing of the items by applying different paddings to the item element: half of the gap to be reached, to the left, the right and the bottom padding and the full gap to the top. The latter, to achieve pushing the previous element far enough away so as not to see anything of the shadow. But because this would mean that the horizontal gap would be too large by half, we have to use a negative margin to pull the element up by that amount. This in turn would mean, that the first item element in the list would be too high by the now defined negative top margin, which we correct again by using item:first-child. 1234567891011121314151617181920212223:root { --gap: 20px; --gap-half: calc(var(--gap) / 2); /* ... more variables */}.wrapper { /* ...more styles */ column-gap: 0;}.item { /* ... BASIC STYLES */ /* LAYOUT STYLES */ width: auto; margin: var(--gap-half-negative) 0 0; padding: var(--gap) var(--gap-half) var(--gap-half); /* COLUMN STYLES */ break-inside: avoid-column;}.item:first-child { margin-top: 0;} Here is the complete solution as a pen: ConclusionMaybe the Chromium team will fix the bug as soon as possible, to no longer have to rely on this hack, but the really best solution would be, to finish the work already started on the true grid masonry, which currently only Firefox offers behind a flag. More Info caniuse.com: Can I Use: CSS property: grid-template-rowsRyan (dev.to): Creating A Responsive Masonry Layout Using The CSS column-count PropertyMDN WebDocs: Masonry LayoutRachel Andrew (Smashing Magazine): Native CSS Masonry Layout In CSS GridSyed Umar Anis: Create Masonry Layout with CSSChris Coyier (CSS-Tricks): Approaches for a CSS Masonry LayoutJonas (Kulturbanause): Responsive Masonry Layouts mit CSS erstellen (German)","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"}]},{"title":"GitHub Tag Plugins for Hexo","subtitle":null,"date":"2021-12-29","updated":"2021-12-29","path":"post/GitHub-Tag-Plugins-for-Hexo/","permalink":"https://kiko.io/post/GitHub-Tag-Plugins-for-Hexo/","excerpt":"Currently I’m working on improving my projects section by linking to some of my projects hosted on Github. One idea is to display the Github README there. Playing around with the GitHub API is fun and so I created two new Hexo Tag Plugins that I don’t want to deprive you of and that extend the Hexo Tag Plugin Collection. GitHub READMEGitHub User & Repo Card","keywords":"im working improving projects section linking hosted github idea display readme playing api fun created hexo tag plugins dont deprive extend plugin collection readmegithub user repo card","text":"Currently I’m working on improving my projects section by linking to some of my projects hosted on Github. One idea is to display the Github README there. Playing around with the GitHub API is fun and so I created two new Hexo Tag Plugins that I don’t want to deprive you of and that extend the Hexo Tag Plugin Collection. GitHub READMEGitHub User & Repo Card GitHub READMEGets the README.md file from a repo and shows it in an expandable detail tag. Usage Example: 1{% github_readme "kristofzerbe" "hexo-tag-plugins" "README for 'hexo-tag-plugins' on GitHub" %} Live Output: README for 'hexo-tag-plugins' on GitHub Hexo Tag PluginsHexo Tag Plugin Collection from kiko.io IntroductionHexo is a Markdown based SSG (Static Site Generator). Because the Mardown syntax is limited for good reason, Hexo has tag plugins you can use in your content to simplify and centralize complex structures, instead of writing pure HTML. For more information, see hexo.io/docs/tag-plugins. This project is a growing collection of tag plugins that I have developed and use for my blog kiko.io. Some of them are quite simple, others are more complex, but overall maybe helpful for you. Installation / UsingThere is no automatic installation of all tag plugins via NPM or other package managers, because every tag plugin stands for itself and you can pick the one you need simply by copying the appropriate JS file into your Hexo’s script folder: THEMES / <YOUR-THEME> / SCRIPTS. For every tag plugin in the list below I provide a Visual Studio Code Snippet to quickly insert a plugin into the content via the hotkey Ctrl+Space. To use the snippest create a new .code-snippets file in your projects .vscode folder and insert the snippets of all tag plugins you have downloaded into your project. Parameter Description SyntaxEvery tag plugin includes a description in the header how to use it. The syntax is as follows: Syntax Description param1 mandatory parameter [param2] optional parameter [param2=default] optional parameter with default value param:(option1,option2) parameter option list to choose one option ..."value1|value2" pipe delimitered array of values Plugins Anchor Anchorlist Alertbox Alternative Blockquote Blockquote Details Codepen CodeSandbox Download Link Github Readme GitHub User & Repo Card Image Compare Image Link Image Slide Indiepen More Info Image Masonry AnchorA simple anchor element as A- or HR-Tag as jump target for example from a Anchorlist. Files: tag-anchor.js Syntax: {% anchor "anchorId" elementType:(A,HR) %}`` Parameters: No Parameter optional/default Description 1 anchorId - String to define the anchor id 2 elementType - Type of tag to render; select out of A or HR Usage Example: {% anchor "my-anchor" HR %} Output: <hr id="my-anchor"> VS Code Snippet: "hexo.kiko-io.anchor": { "scope": "markdown", "prefix": "hexo.kiko-io.anchor", "body": [ "{% anchor \\"${1:anchorId}\\" ${2|A,HR|} %}" ], "description": "Insert kiko.io's anchor" } AnchorlistCreates an overview of all anchors in the content with jump links. Files: tag-anchorlist.js Syntax: {% anchorlist ..."title|anchorId" %} Parameters: No Parameter optional/default Description 1 ..."title|anchorId" - List of pipe separated items with referencing title and anchor id Usage Example: {% anchorlist "My First Anchor|a1" "My Second Anchor|a2" %} Output: <ul class="anchorlist"> <li data-anchor="#a1"> <a href="#a1">My First Anchor</a> </li> <li data-anchor="#a2"> <a href="#a2">My Second Anchor</a> </li> </ul> VS Code Snippet: "hexo.kiko-io.anchorlist": { "scope": "markdown", "prefix": "hexo.kiko-io.anchorlist", "body": [ "{% anchorlist ${1:...\\"title|anchorId\\"} %}" ], "description": "Insert kiko.io's anchorlist" } AlertboxRenders a iconized colored box with text for warnings or with some special information. 6 styles are provided: Exclamation, Question, Warning, Info, Success and Note. Files: tag-alertbox.js tag-alertbox.css Prequisites: The icons are from the font FontAwesome Free Solid, you need to reference in your CSS either from your project or from a CDN. You will find such references in the file tag-alertbox.css, together with all other necessary styles. Syntax: {% alertbox alertType:(exclamation,question,warning,info,success,note) %} content {% endalertbox %}`` Parameters: No Parameter optional/default Description 1 alertType - Type of the alert (icon and color); select out of exclamation, question, warning, info, success or note content is not a parameter, but Markdown to render. Usage Example: {% alertbox warning %} Something has failed! {% endalertbox %} Output: <div class="alertbox alertbox-warning"> <p>Something has failed!</p> </div> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#alertbox VS Code Snippet: "hexo.kiko-io.alertbox": { "scope": "markdown", "prefix": "hexo.kiko-io.alertbox", "body": [ "{% alertbox ${1|exclamation,question,warning,info,success,note|} %}", "${2:content}", "{% endalertbox %}" ], "description": "Insert kiko.io's alertbox" } Alternative BlockquoteAn alternative blockquote tag plugin for quotes with citator and reference url. Files: tag-blockquote_alt.js Syntax: {% blockquote_alt "cite" ["citeUrl"] %} quote {% endblockquote_alt %} Parameters: No Parameter optional/default Description 1 cite Author of the quote 2 citeUrl yes Url to the quote Usage Example: {% blockquote_alt "Anonymous" "https://en.wikipedia.org/wiki/Lorem_ipsum" %} Lorem ipsum dolor sit amet... {% endblockquote_alt %} Output: <div> <blockquote> <p>Lorem ipsum dolor sit amet…</p> </blockquote> <cite> <a href="https://en.wikipedia.org/wiki/Lorem_ipsum">--- Anonymous</a> </cite> </div> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#blockquote_alt VS Code Snippet: "hexo.kiko-io.blockquote": { "scope": "markdown", "prefix": "hexo.kiko-io.blockquote", "body": [ "{% blockquote_alt \\"${1:cite}\\" [\\"${2:citeUrl}\\"] %}", "${3:content}", "{% endblockquote_alt %}" ], "description": "Insert kiko.io's blockquote" } Blockquote DetailsBlockquote including summary, citator and reference url, wrapped in a details tag. Files: tag-blockquote_details.js Syntax: {% blockquote_details "summary" "cite" ["citeUrl"] %} quote {% endblockquote_details %} Parameters: No Parameter optional/default Description 1 summary - Summary of the quote 2 cite - Author of the quote 3 citeUrl yes Url to the quote quote is not a parameter, but Markdown to render. Usage Example: {% blockquote_details "Lorem ipsum" "Anonymous" "https://en.wikipedia.org/wiki/Lorem_ipsum" %} Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. {% endblockquote_details %} Output: <details> <summary>Lorem ipsum</summary> <blockquote> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. </p> </blockquote> <cite> <a href="https://en.wikipedia.org/wiki/Lorem_ipsum">--- Anonymous</a> </cite> </details> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#blockquote_details VS Code Snippet: "hexo.kiko-io.blockquote_details": { "scope": "markdown", "prefix": "hexo.kiko-io.blockquote_details", "body": [ "{% blockquote_details \\"${1:summary}\\" \\"${2:cite}\\" [\\"${3:citeUrl}\\"] %}", "${4:quote}", "{% endblockquote_details %}" ], "description": "Insert kiko.io's blockquote_details" } CodepenEmbedding a pen from codepen.io. Files: tag-codepen.js Prequisites: You need following configuration section in your _config.yml: # Codepen Defaults codepen: user_id: "your-name" default_tab: "js" height: 400 width: "100%" Syntax: {% codepen "slugHash" "title" [defaultTab:(html,js,css)] [height] ["width"] %} Parameters: No Parameter optional/default Description 1 slugHash - Codepens SlugHash 2 title - Title 3 defaultTab js Default tab to show ; select out of html, js or css 4 height 300 Height as number 5 width “100%” Width as CSS value Usage Example: {% codepen "abjJNYE" "Lorem Ipsum" html %} Output: <iframe height="400" id="codepen-abjJNYE" class="codepen" src="https://codepen.io/kristofzerbe/embed/abjJNYE?height=400&default-tab=html,result&theme-id=light" style="width: 100%;" scrolling="no" title="Codepen: Lorem Ipsum" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true"> </iframe> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#codepen VS Code Snippet: "hexo.kiko-io.codepen": { "scope": "markdown", "prefix": "hexo.kiko-io.codepen", "body": [ "{% codepen \\"${1:slugHash}\\" \\"${2:title}\\" [${3|html,js,css|}] [${4:height}] [\\"${5:width}\\"] %}" ], "description": "Insert kiko.io's codepen" } CodeSandboxEmbedding a sandbox from CodeSandbox. Files: tag-codesandbox.js Syntax: {% codesandbox "slugHash" "title" [height] ["width"] %} Parameters: No Parameter optional/default Description 1 slugHash - Sandbox’ SlugHash 2 title - Title 3 height 500 Height as number 4 width “100%” Width as CSS value Usage Example: {% codesandbox "cool-shamir-de613" "Lorem Ipsum" 300 %} Output: <iframe src="https://codesandbox.io/embed/cool-shamir-de613?fontsize=14&theme=light" style="width:100%; height:300px; border:0; overflow:hidden;" title="Lorem Ipsum" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"> </iframe> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#codesandbox VS Code Snippet: "hexo.kiko-io.codesandbox": { "scope": "markdown", "prefix": "hexo.kiko-io.codesandbox", "body": [ "{% codesandbox ${1:\\"slugHash}\\" \\"${2:title}\\" [${3:height}] [\\"${4:width}\\"] %}" ], "description": "Insert kiko.io's codesandbox" } Download LinkButton link for downloading an asset file, with additional caption (“Download <additionalCaption> <assetFile>”). Files: tag-download-link.js Syntax: {% download_link "assetFile" ["additionalCaption"] %} Parameters: No Parameter optional/default Description 1 assetFile Asset file name to download 2 additionalCaption yes Additional caption between “Download “ and file name Usage Example: {% download_link "example-image_ORIGINAL.jpg" "Photo" %} Output: <p class="download-link"> <a class="button" href="example-image_ORIGINAL.jpg" download=""> Download Photo <strong>example-image_ORIGINAL.jpg</strong> </a> </p> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#download-link VS Code Snippet: "hexo.kiko-io.download_link": { "scope": "markdown", "prefix": "hexo.kiko-io.download_link", "body": [ "{% download_link \\"${1:assetFile}\\" \\"${2:additionalCaption}\\" %}" ], "description": "Insert kiko.io's download_link" } Github ReadmeGets the README file of a Github repo and renders its Markdown as HTML in a detail tag. Files: tag-github-readme.js Prequisites: You need to have installed the Axios package in your project. The HTML output has no styles, therefore you need some in your CSS for .github-readme. Syntax: {% github_readme "user" "repo" ["summary"] %} Parameters: No Parameter optional/default Description 1 user Name of the GitHub user 2 repo Name of the GitHub repo 3 summary “Project README on Github” Caption of the DETAILS element Usage Example: {% github_readme "kristofzerbe" "hexo-tag-plugins" %} Output: <details class="github-readme"> <summary>Project README on Github</summary> <div> <!-- Content of the README file converted to HTML --> </div> </details> VS Code Snippet: "hexo.kiko-io.github_readme": { "scope": "markdown", "prefix": "hexo.kiko-io.github_readme", "body": [ "{% github_readme \\"${1:user}\\" \\"${2:repo}\\" \\"${3:[summary]}\\" %}" ], "description": "Insert kiko.io's github_readme" } See a live example at https://kiko.io/post/GitHub-Tag-Plugins-for-Hexo/#readme GitHub User & Repo CardRenders a card-like info panel, with full information about a GitHub repo and its creator, the GitHub user. Files: tag-github-user-and-repo-card.js tag-github-user-and-repo-card.css Prequisites: You need to have installed the Axios package in your project. Syntax: {% github_user_and_repo_card "user" "repo" ["cardWidth"] ["userheight"] ["avatarSize"] %} Parameters: No Parameter optional/default Description 1 user Name of the GitHub user 2 repo Name of the GitHub repo 3 cardWidth “400px” Maximum width of the card; Minimum is 300px 4 userheight “120px” Height of the upper user panel 5 avatarSize “90px” Size of the avatar image as CSS value Usage Example: {% github_user_and_repo_card "kristofzerbe" "hexo-tag-plugins" "500px" %} Output: <!-- see tag-github-user-and-repo-card.html --> VS Code Snippet: "hexo.kiko-io.github_user_and_repo_card": { "scope": "markdown", "prefix": "hexo.kiko-io.github_user_and_repo_card", "body": [ "{% github_user_and_repo_card \\"${1:user}\\" \\"${2:repo}\\" \\"${3:[cardWidth]}\\" \\"${4:[userhight]}\\" \\"${5:[avatarSize]}\\" %}" ], "description": "Insert kiko.io's github_user_and_repo_card" } See a live example at https://kiko.io/post/GitHub-Tag-Plugins-for-Hexo/#user-and-repo-card Image CompareComparing two asset images side-by-side with the aid of the JS library Image Compare Viewer. Files: tag-image-compare.js Prequisites: As this tag plugin relies on an external JS library, the files image-compare-viewer.js and image-compare-viewer.css (or its minified versions) must be loaded in the header of the web page. Syntax: {% image_compare "imgFileOriginal" "imgFileModified" "descriptionModified" [orientation=vertical] %} Parameters: No Parameter optional/default Description 1 imgFileOriginal - Original asset image file name 2 imgFileModified - Modified asset image file name 3 descriptionModified - Description 4 orientation null Vertical orientation Mode; set vertical to select Usage Example: {% image_compare "example-image_ORIGINAL.jpg" "example-image_PRESET.jpg" "Lightroom Preset" %} Output: <div id="image-compare-1yrasq"> <img class="image-compare image-original" src="/post/my-post/example-image_ORIGINAL.jpg" alt="" /> <img class="image-compare image-modified" src="/post/my-post/example-image_PRESET.jpg" alt="" /> </div> <script> var themeColor = "#ffffff"; if (localStorage.getItem("theme") === 'dark') { themeColor = "#222222" } new ImageCompare(document.getElementById("image-compare-1yrasq"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Lightroom Preset', onHover: true, } }).mount(); </script> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#image-compare VS Code Snippet: "hexo.kiko-io.image_compare": { "scope": "markdown", "prefix": "hexo.kiko-io.image_compare", "body": [ "{% image_compare \\"${1:imgFileOriginal}\\" \\"${2:imgFileModified}\\" \\"${3:modDesc}\\" [\\"${4|vertical|}\\"] %}" ], "description": "Insert kiko.io's image_compare" } Image LinkRenders an image including ALT attribute within a link. Files: tag-image-link.js Syntax: {% image_link "assetImg" "url" "alt" %} Parameters: No Parameter optional/default Description 1 assetImg - Asset image file name 2 url - Url to link to 3 alt - Alternate text Usage Example: {% image_link "kiko-io-screenshot.png" "http://kiko.io" "Blog kiko.io" %} Output: <a href="http://kiko.io"> <img src="kiko-io-screenshot.png" alt="Blog kiko.io"> </a> VS Code Snippet: "hexo.kiko-io.image_link": { "scope": "markdown", "prefix": "hexo.kiko-io.image_link", "body": [ "{% image_link \\"${1:assetImg}\\" \\"${2:url}\\" \\"${3:alt}\\" %}" ], "description": "Insert kiko.io's image_link" } Image SlideShows multiple images within a slider with the aid of the JS library Tiny Slider. Files: tag-image-slide.js Prequisites: As this tag plugin relies on an external JS library, the files tiny-slider.js and tiny-slider.css (or its minified versions) must be loaded in the header of the web page. The CSS file doesn’t include styles for the .tns-nav and its controls, but you can use the following to extend the original CSS: .tns-nav { text-align: center; margin: 10px 0; } .tns-nav > [aria-controls] { width: 12px; height: 12px; padding: 0; margin: 0 5px; border-radius: 50%; background: #ddd; border: 0; } .tns-nav > .tns-nav-active { background: #999; } Syntax: {% image_slide ..."assetImg|title" %} Parameters: No Parameter optional/default Description 1 ..."assetImg|title" List of pipe separated items with asset image file and title Usage Example: {% image_slide "example-image_ORIGINAL.jpg|Original" "example-image_PRESET.jpg|Lightroom Preset" %} Output: <div class="image-slider" id="image-slide-w7jgxk"> <div> <img src="/post/my-post/example-image_ORIGINAL.jpg" alt="Original" /> </div> <div> <img src="/post/my-post/example-image_PRESET.jpg" alt="Lightroom Preset" /> </div> </div> <script> tns({ container: "#image-slide-w7jgxk", items: 1, slideBy: "page", controls: false, nav: true }); </script> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#image-slide VS Code Snippet: "hexo.kiko-io.image_slide": { "scope": "markdown", "prefix": "hexo.kiko-io.image_slide", "body": [ "{% image_slide ${1:...\\"assetImg|title\\"} %}" ], "description": "Insert kiko.io's image_slide" } IndiepenEmbedding a “local” pen (index.html, main.js and styles.css stored in an asset subfolder) via Indiepen. Files: tag-indiepen.js Prequisites: You need following configuration section in your _config.yml: # Indiepen Defaults indiepen: default_tab: "result" height: 450 Syntax: {% indiepen "subfolder" [height] [defaultTab:(result,html,css,js)] %} Parameters: No Parameter optional/default Description 1 subfolder Asset subfolder with indiepen files 2 defaultTab result Default tab to show ; select out of result, html, js or css 3 height 450 Height as number Usage Example: {% indiepen "my-pen" 300 html %} Output: <iframe class="indiepen" src="https://indiepen.tech/embed/?url=https%3A%2F%2Fmy-blog.com%2Fpost%2Fmy-post%2Fmy-pen&tab=html" style="width: 100%; overflow: hidden; display: block; border: 0;" title="Indiepen Embed" loading="lazy" width="100%" height="300"> </iframe> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#indiepen VS Code Snippet: "hexo.kiko-io.indiepen": { "scope": "markdown", "prefix": "hexo.kiko-io.indiepen", "body": [ "{% indiepen \\"${1:subfolder}\\" ${2:height} ${3|result,html,css,js|} %}" ], "description": "Insert kiko.io's indiepen" } More InfoRenders a list of related, informative links regarding a post. Files: tag-moreinfo.js Syntax: {% moreinfo '{ "list": [ [ "publisher", "title", "url" ] ]}' %} Usage Example: {% moreinfo '{ "list": [ [ "Wikipedia", "Markdown", "https://en.wikipedia.org/wiki/Markdown" ], [ "Markdown Guide", "Basic Syntax", "https://www.markdownguide.org/basic-syntax/" ], [ "Daring Fireball", "Markdown: Syntax", "https://daringfireball.net/projects/markdown/syntax" ] ]}' %} Output: <ul class="moreinfo-list"> <li>Wikipedia: <a href="https://en.wikipedia.org/wiki/Markdown">Markdown</a></li> <li>Markdown Guide: <a href="https://www.markdownguide.org/basic-syntax/">Basic Syntax</a></li> <li>Daring Fireball: <a href="https://daringfireball.net/projects/markdown/syntax">Markdown: Syntax</a></li> </ul> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#more-info VS Code Snippet: "hexo.kiko-io.moreinfo": { "scope": "markdown", "prefix": "hexo.kiko-io.moreinfo", "body": [ "{% moreinfo '{ \\"list\\": [", " [ ${1:\\"publisher\\"}, ${2:\\"title\\"},", " ${3:\\"url\\"} ]$0", "]}' %}" ], "description": "Insert kiko.io's moreinfo" } To insert one more item to the list, use: "hexo.kiko-io.moreinfo.item": { "scope": "markdown", "prefix": "hexo.kiko-io.moreinfo.item", "body": [ "[ ${1:\\"publisher\\"}, ${2:\\"title\\"},", "${3:\\"url\\"} ]$0" ], "description": "Insert kiko.io's moreinfo item" } Image MasonryShows multiple images in a masonry grid with the aid of the JS library Macy.js. Files: tag-image-masonry.js Prequisites: As this tag plugin relies on an external JS library, the library file macy.js must be loaded in the header of the web page. Syntax: {% image_masonry ..."assetImg|title" %} Parameters: No Parameter optional/default Description 1 ..."assetImg|title" List of pipe separated items with asset image file and title Usage Example: {% image_masonry "example-image-1.jpg|First Image" "example-image-2.jpg|Second Image" "example-image-3.jpg|Third Image" "example-image-4.jpg|Fourth Image" "example-image-5.jpg|Fifth Image" "example-image-6.jpg|Sixth Image" "example-image-7.jpg|Seventh Image" "example-image-8.jpg|Eighth Image" %} Output: <div id="#image-masonry-z8katm"> <div><img src="example-image-1.jpg" alt="First Image"></div> <div><img src="example-image-2.jpg" alt="Second Image"></div> <div><img src="example-image-3.jpg" alt="Third Image"></div> <div><img src="example-image-4.jpg" alt="Fourth Image"></div> <div><img src="example-image-5.jpg" alt="Fifth Image"></div> <div><img src="example-image-6.jpg" alt="Sixth Image"></div> <div><img src="example-image-7.jpg" alt="Seventh Image"></div> <div><img src="example-image-8.jpg" alt="Eighth Image"></div> </div> <script> let macy = new Macy({ container: "#image-masonry-z8katm", trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 1024: { margin: { x: 8, y: 8 }, columns: 4 }, 768: 3 } }); </script> See a live example at https://kiko.io/post/Image-Masonry-Tag-Plugin-for-Hexo/ VS Code Snippet: "hexo.kiko-io.image_masonry": { "scope": "markdown", "prefix": "hexo.kiko-io.image_masonry", "body": [ "{% image_masonry ${1:...\\"assetImg|title\\"} %}" ], "description": "Insert kiko.io's image_masonry" } History2023-09-01 Image Masonry added 2021-12-29 Description of parameters added Github Readme added GitHub User & Repo Card added 2021-12-12 Initial Commit LicenseMIT : http://opensource.org/licenses/MIT See https://github.com/kristofzerbe/hexo-tag-plugins#github-readme for more details. GitHub User & Repo CardRenders a card-like info panel, with full information about a GitHub repo and its creator, the GitHub user. Usage Example: 1{% github_user_and_repo_card "kristofzerbe" "hexo-tag-plugins" "500px" %} Live Output: #gh-card-hexo-tag-plugins { --gh-card-width: 500px; --gh-user-height: 120px; --gh-avatar-size: 90px; } Kristof Zerbe kristofzerbe 33 Repos 9 Followers 3 2 1 0 hexo-tag-plugins Hexo Tag Plugin Collection from kiko.io JavaScript65.1% HTML18.2% CSS16.7% MIT License See https://github.com/kristofzerbe/hexo-tag-plugins#github-user--repo-card for more details.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"}]},{"title":"Discoveries #15 - Self Hosted","subtitle":null,"series":"Discoveries","date":"2021-12-25","updated":"2021-12-25","path":"post/Discoveries-15-Self-Hosted/","permalink":"https://kiko.io/post/Discoveries-15-Self-Hosted/","excerpt":"Especially on Github you can find amazing open source solutions for self hosting, that makes it unnecessary to rely on web services from companies you did not know. In this issue of Discoveries I would like to introduce you to a few of them … Happy Holidays AppFlowyBangle.ioCal.com (formerly Calendso)RSS-proxyFreshRSSStatsig's Status Pagechangedetection.ioHomerFiddlyFileDrop","keywords":"github find amazing open source solutions hosting makes unnecessary rely web services companies issue discoveries introduce … happy holidays appflowybangleiocalcom calendsorss-proxyfreshrssstatsig's status pagechangedetectioniohomerfiddlyfiledrop","text":"Especially on Github you can find amazing open source solutions for self hosting, that makes it unnecessary to rely on web services from companies you did not know. In this issue of Discoveries I would like to introduce you to a few of them … Happy Holidays AppFlowyBangle.ioCal.com (formerly Calendso)RSS-proxyFreshRSSStatsig's Status Pagechangedetection.ioHomerFiddlyFileDrop AppFlowy by -unknown- https://github.com/AppFlowy-IO/appflowy AppFlowy.IO is a Notion clone, written in Flutter and Rust und runs on macOS (with installer), Windows and Linux. Bangle.io by -unknown- https://github.com/bangle-io/bangle-io Bangle.io is a web based note taking platform, like Notion, but local only. It is written in TypeScript and relies on Markdown and works also offline. Cal.com (formerly Calendso) by Cal.com Team https://github.com/calendso/calendso Cal.com is an alternative scheduling service to Calendsy, driven by a company as a service, but also available as Open Source for selfhosting. It is written in JavaScript (Next.js). RSS-proxy by Github User 'damoeb' https://github.com/damoeb/rss-proxy RSS-proxy (demo), written in TypeScript, is a web service to create ATOM, RSS or JSON feeds by analyzing a websites static HTML structure. Helpful for websites, that doesn’t provide a feed. FreshRSS by Alexandre Alapetite & Others https://github.com/FreshRSS/FreshRSS FreshRSS (demo) is an alternative to feed.ly and other online RSS readers and aggregators, written in PHP. It supports custom tags, push notifications and extensions and has a CLI. Statsig's Status Page by statsig.com Team https://github.com/statsig-io/statuspage/ This open source status page solution (demo) uses Github actions to run a sh script every hour against configurable URL’s to check their status and log it in a static index.html. changedetection.io by Github User 'dgtlmoon' https://github.com/dgtlmoon/changedetection.io Web solution for monitoring configurable websites or JSON API’s for changes, written in Python. It detects changes, notifies and shows the differences. Homer by Bastien Wirtz https://github.com/bastienwirtz/homer A simple, but nice dashboard for your servers and services, configurable with YAML and written in Vue. Fiddly by Sara Vieira https://github.com/SaraVieira/fiddly Fiddly creates customizable HTML pages out of your Github projects README files for hosting on Github Pages , Netlify or others under a dedicated domain. FileDrop by Khodadad (Adrian) Nouchin https://github.com/Xtrendence/FileDrop FileDrop is an application to share files in the same network through a browser. It is written in JavaScript and Electron (Server) and is using WebSocket for encrypted transport. Releases are available for Windows, macOS and Linux.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Hexo Tag Plugin Collection","subtitle":"All tag plugins used for kiko.io available on Github","date":"2021-12-12","updated":"2021-12-12","path":"post/Hexo-Tag-Plugin-Collection/","permalink":"https://kiko.io/post/Hexo-Tag-Plugin-Collection/","excerpt":"Since day one of this blog I use Tag Plugins, sometimes as NPM packages from other developers, sometimes developed by myself. The latter have grown significantly over time and I want to share them with you by publishing them in a Github project called hexo-tag-plugins, where you can download and use those you need on extending your own Hexo based blog. On the Github page you can find all the info on how to use the plugins. In this article I will only briefly introduce them: AnchorAnchorlistAlertboxAlternative BlockqouteBlockquote DetailsCodepenCodeSandboxDownload LinkImage CompareImage LinkImage SlideIndiepenMore Info","keywords":"day blog tag plugins npm packages developers developed grown significantly time share publishing github project called hexo-tag-plugins download extending hexo based page find info article briefly introduce anchoranchorlistalertboxalternative blockqouteblockquote detailscodepencodesandboxdownload linkimage compareimage slideindiepenmore","text":"Since day one of this blog I use Tag Plugins, sometimes as NPM packages from other developers, sometimes developed by myself. The latter have grown significantly over time and I want to share them with you by publishing them in a Github project called hexo-tag-plugins, where you can download and use those you need on extending your own Hexo based blog. On the Github page you can find all the info on how to use the plugins. In this article I will only briefly introduce them: AnchorAnchorlistAlertboxAlternative BlockqouteBlockquote DetailsCodepenCodeSandboxDownload LinkImage CompareImage LinkImage SlideIndiepenMore Info AnchorAnchor element as A- or HR-Tag as jump target for example from a Anchorlist. Usage Example: 1{% anchor "my-anchor" HR %} Live Output: 1<hr id="my-anchor"> See https://github.com/kristofzerbe/hexo-tag-plugins#anchor for more details. AnchorlistCreates an overview of all anchors in the content with jump links. Usage Example: 1234{% anchorlist "My First Anchor|a1" "My Second Anchor|a2"%} Live Output: My First AnchorMy Second Anchor See https://github.com/kristofzerbe/hexo-tag-plugins#anchorlist for more details. AlertboxRenders a iconized colored box with text for warnings or with some special information. 6 styles are provided: Exclamation, Question, Warning, Info, Success and Note. This plugin uses a FontAwesome font for the icons and some styles that also need to be included in your Hexo project. Usage Example: 123{% alertbox warning %}Something has failed!{% endalertbox %} Live Output: Something has failed! Uuh, keep attention! Everything’s fine What’s up? Some important information Just as note… See https://github.com/kristofzerbe/hexo-tag-plugins#alertbox for more details. Alternative BlockquoteAn alternative blockquote tag plugin for quotes with citator and reference url. Usage Example: 123{% blockquote_alt "Anonymous" "https://en.wikipedia.org/wiki/Lorem_ipsum" %}Lorem ipsum dolor sit amet...{% endblockquote_alt %} Live Output: Lorem ipsum dolor sit amet… --- Anonymous See https://github.com/kristofzerbe/hexo-tag-plugins#alternative-blockquote for more details. Blockquote DetailsBlockquote including summary, citator and reference url, wrapped in a details tag. Usage Example: 123{% blockquote_details "Lorem ipsum" "Anonymous" "https://en.wikipedia.org/wiki/Lorem_ipsum" %}Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.{% endblockquote_details %} Live Output: Lorem ipsum Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. --- Anonymous See https://github.com/kristofzerbe/hexo-tag-plugins#blockquote-details for more details. CodepenEmbedding a pen from Codepen. Usage Example: 1{% codepen "abjJNYE" "Lorem Ipsum" html 250 %} Live Output: See https://github.com/kristofzerbe/hexo-tag-plugins#codepen for more details. CodeSandboxTag Plugin for embedding a sandbox from CodeSandbox. Usage Example: 1{% codesandbox "cool-shamir-de613" "Lorem Ipsum" 300 %} Live Output: See https://github.com/kristofzerbe/hexo-tag-plugins#codesandbox for more details. Download LinkButton link for downloading an asset file, with additional caption (“Download <additionalCaption> <assetFile>”). Usage Example: 1{% download_link "example-image_ORIGINAL.jpg" "Photo" %} Live Output: Download Photo example-image_ORIGINAL.jpg See https://github.com/kristofzerbe/hexo-tag-plugins#download-link for more details. Image CompareComparing two images side-by-side with the aid of the JS library Image Compare Viewer. Usage Example: 12345{% image_compare "example-image_ORIGINAL.jpg" "example-image_PRESET.jpg" "Lightroom Preset" %} Live Output: var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-98v11q\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Lightroom Preset', onHover: true, } }).mount(); See https://github.com/kristofzerbe/hexo-tag-plugins#image-compare for more details. Image LinkRenders an image including ALT attribute within a link. Usage Example: 1{% image_link "kiko-io-screenshot.png" "http://kiko.io" "Blog kiko.io" %} Live Output: See https://github.com/kristofzerbe/hexo-tag-plugins#image-link for more details. Image SlideShows multiple images within a slider with the aid of the JS library Tiny Slider. Usage Example: 1234{% image_slide "example-image_ORIGINAL.jpg|Original" "example-image_PRESET.jpg|Lightroom Preset"%} Live Output: tns({ container: \"#image-slide-xckbdd\", items: 1, slideBy: \"page\", controls: false, nav: true }); See https://github.com/kristofzerbe/hexo-tag-plugins#image-slide for more details. IndiepenEmbedding a “local” pen (index.html, main.js and styles.css stored in an asset subfolder) via Indiepen. Usage Example: 1{% indiepen "indiepen-example" 300 html %} Live Output: See https://github.com/kristofzerbe/hexo-tag-plugins#indiepen for more details. More InfoRenders a list of related, informative links regarding a post. Usage Example: 12345678{% moreinfo '{ "list": [ [ "Wikipedia", "Markdown", "https://en.wikipedia.org/wiki/Markdown" ], [ "Markdown Guide", "Basic Syntax", "https://www.markdownguide.org/basic-syntax/" ], [ "Daring Fireball", "Markdown: Syntax", "https://daringfireball.net/projects/markdown/syntax" ]]}' %} Live Output: Wikipedia: MarkdownMarkdown Guide: Basic SyntaxDaring Fireball: Markdown: Syntax See https://github.com/kristofzerbe/hexo-tag-plugins#more-info for more details.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"}]},{"title":"Tringula And The Beauty Of Mathematics","subtitle":"Triangula is a great tool for artfully breaking up images into polygons. Another tool also makes it possible to use them as website placeholders.","series":"Golem","date":"2021-12-07","updated":"2021-12-07","path":"post/Tringula-And-The-Beauty-Of-Mathematics/","permalink":"https://kiko.io/post/Tringula-And-The-Beauty-Of-Mathematics/","excerpt":"This post is a new version of Triangulate your images with Triangula and the first in a series of articles published on the German news site golem.de. When talking about triangulation, non-mathematicians generally understand it as a geometric method for measuring distances. Roughly speaking, two known points in space can be used to calculate a third via the angles to it. In one or the other Hollywood flick of the genres war or spy movie you have surely come across this. However, triangulation also refers to the division of a surface into triangles or, more generally, the description of an object by means of polygons. It is used in topology and land surveying, but also in imaging methods of modeling. How wonderfully this field of mathematics can be applied to photos is shown by the GitHub user RyanH with his program Triangula written in Go, which first roughly splits a given JPG or PNG image into triangles and then refines it further and further via mutations. Among other things, you can specify how many points you want to start with and how many mutations the program should perform. It is also possible to calculate the new image using hexagons instead of classic triangles.","keywords":"post version triangulate images triangula series articles published german news site golemde talking triangulation non-mathematicians generally understand geometric method measuring distances roughly speaking points space calculate angles hollywood flick genres war spy movie surely refers division surface triangles description object means polygons topology land surveying imaging methods modeling wonderfully field mathematics applied photos shown github user ryanh program written splits jpg png image refines mutations things start perform hexagons classic","text":"This post is a new version of Triangulate your images with Triangula and the first in a series of articles published on the German news site golem.de. When talking about triangulation, non-mathematicians generally understand it as a geometric method for measuring distances. Roughly speaking, two known points in space can be used to calculate a third via the angles to it. In one or the other Hollywood flick of the genres war or spy movie you have surely come across this. However, triangulation also refers to the division of a surface into triangles or, more generally, the description of an object by means of polygons. It is used in topology and land surveying, but also in imaging methods of modeling. How wonderfully this field of mathematics can be applied to photos is shown by the GitHub user RyanH with his program Triangula written in Go, which first roughly splits a given JPG or PNG image into triangles and then refines it further and further via mutations. Among other things, you can specify how many points you want to start with and how many mutations the program should perform. It is also possible to calculate the new image using hexagons instead of classic triangles. tns({ container: \"#image-slide-l3yoz3\", items: 1, slideBy: \"page\", controls: false, nav: true }); The result is stylized images of the original, which can be used as a chic desktop background, for example. Due to the abstraction of the actual motif, such images are also very suitable as header images on websites, such as blogs, if the image should not distract from the actual content. Such an image has a great closeness to the original, but looks more like art. Besides PNG, SVG (Scalable Vector Graphics) is also available as output format, which makes sense because SVG is an XML format and the polygons calculated by the program can also be written away directly as corresponding polygon entries as text. The generated SVG files are thereby smaller by a factor of 30, depending on the original, and thus fit in many places much better with tight bandwidths or used-up consumption limits on the Internet. Using Triangula on the command lineRyan offers his program Triangula also as CLI version, which is called via the command line. The process of triangulating is separated into two parts: First you create the desired abstraction of an original image via the parameter run, which is stored in a JSON file on the hard disk and in a second step you create either a PNG or an SVG as output file via render and the specification of the JSON file. Since the command line tool has the same options as the UI version, it is great for automating the processing of the images used when building a website, for example. It also makes it much easier to turn an entire folder of images into such artistic abstractions. Using triangulated images as placeholders on websitesDepending on how good the Internet connection is and how well the developers brain of a website has worked, it can take a while until the browser of the smartphone has loaded the x-megabyte header or illustration image to finally be able to display it. It gets really annoying when no space has been reserved on the website for the image beforehand and the text that you have started to read suddenly jumps away. In any case, the solution is to always use images that are as small as possible and adapted to the device, but they still have a certain file size if they are to look good. To prevent text jumping, nowadays one usually uses gray placeholders with or without loading bars to signal to the user that something will be displayed at this point shortly. Ryan has found an amazingly effective solution for this as well: tip - Triagulated Placeholders - creating the smallest possible triangulated images and blending them using JavaScript. The basis is a frontend, also written in Go, which uses the same algorithms as Triangula and with which the user can process several original images or an entire folder of photos at once via the interface. The output format from tip is not PNG or SVG, but a binary file, which consumes the least amount of memory of all technical possibilities. For comparison the generated file sizes of the example image with resolution 1024 x 660: Image Size Original 386 KB Triangula PNG 223 KB Triangula SVG 40,3 KB tip TRI 3,03 KB These files, with the extension TRI, are referenced by the web developer in the IMG tag of an image on a page in the data-src attribute, and a JavaScript, only 200 lines long, which is also included and delivered with the web page, takes care of immediately displaying this TRI file when the page is loaded and smoothly fading to the original image once it has been loaded. You can hardly make it more beautiful.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"SVG","slug":"SVG","permalink":"https://kiko.io/tags/SVG/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"Discoveries #14","subtitle":null,"series":"Discoveries","date":"2021-11-20","updated":"2021-11-20","path":"post/Discoveries-14/","permalink":"https://kiko.io/post/Discoveries-14/","excerpt":"In this month discoveries you will find some posts and pens regarding CSS and its possibilities. It’s always amazing what can be done with it and what cool solutions can be found on the web. Sticky Definition ListsHow to Detect When a Sticky Element Gets PinnedDark mode in 5 minutes, with inverted lightness variablesFloat an Element to the Bottom CornerNeat Parallax Hero EffectUnderline animationBuilding split text animationsIntrinsic Typography is the Future of Styling Text on the WebCSS morphingCSS Tips","keywords":"month discoveries find posts pens css possibilities amazing cool solutions found web sticky definition listshow detect element pinneddark mode minutes inverted lightness variablesfloat bottom cornerneat parallax hero effectunderline animationbuilding split text animationsintrinsic typography future styling webcss morphingcss tips","text":"In this month discoveries you will find some posts and pens regarding CSS and its possibilities. It’s always amazing what can be done with it and what cool solutions can be found on the web. Sticky Definition ListsHow to Detect When a Sticky Element Gets PinnedDark mode in 5 minutes, with inverted lightness variablesFloat an Element to the Bottom CornerNeat Parallax Hero EffectUnderline animationBuilding split text animationsIntrinsic Typography is the Future of Styling Text on the WebCSS morphingCSS Tips Sticky Definition Lists by Chris Coyier https://css-tricks.com/sticky-definition-lists If you want to have an alphabetical definition list on your website, defined by dl, dt and dd tags, you can give your users a better usability on scrolling through this list by using Chris’ sample. How to Detect When a Sticky Element Gets Pinned by David Walsh https://davidwalsh.name/detect-sticky A very short but useful JS snippet from David Walsh on how to detect if an element is being pinned by CSS’ position:sticky. Dark mode in 5 minutes, with inverted lightness variables by Lea Verou https://lea.verou.me/2021/03/inverted-lightness-variables/ Providing a light and dark mode on websites is almost common today. Lea show us how we can save time on using HSL hue and lightness in CSS. Float an Element to the Bottom Corner by Temani Afif https://css-tricks.com/float-an-element-to-the-bottom-corner/ Placing illustration images in the text is a common way to lighten up a web page. Temani shows us how to place such an image at the bottom of a content element. Neat Parallax Hero Effect by Dominic Magnifico https://codepen.io/magnificode/pen/GpqGOm This Codepen from Dom shows us, how to shrink an hero image on scrolling a page down with little CSS and JavaScript. Underline animation by Aaron Iker https://codepen.io/aaroniker/pen/pojaBvb The default underlined links on web pages are really boring. Aaron gives us on his pen an animated alternative, which uses SVG and CSS. Building split text animations by Adam Argyle https://web.dev/building-split-text-animations/ Animating the title of a web site can be a nice way to add some movement to rigid text. Adam shows us how his approach to animating each character works. Intrinsic Typography is the Future of Styling Text on the Web by Scott Kellum https://css-tricks.com/intrinsic-typography-is-the-future-of-styling-text-on-the-web/ Flexible layouts have their pitfalls, especially in terms of adjusting text sizes. In this post, Adam talks about what he calls intrinsic typography, where text is scaled by using a Bézier curve to improve readability. CSS morphing by Amit Sheen https://codepen.io/amit_sheen/pen/xxqYzvm This pen from Amit is about a pure CSS technique on blending a word into another. Neat effect for countdowns for example. CSS Tips by Marko Denic https://markodenic.com/css-tips/ In this post Marko lists some useful tips on using pure CSS, including Typing Effect, Smooth Scrolling, Truncate text, CSS only modals, custom scrollbars, background-clip text or rounded gradient borders.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"VS Code on the Web","subtitle":"Multiple ways to work with Visual Studio Code online","date":"2021-10-22","updated":"2021-10-22","path":"post/VS-Code-on-the-Web/","permalink":"https://kiko.io/post/VS-Code-on-the-Web/","excerpt":"For most of the years I have been in the IT industry, I have worked with the “fat” Visual Studio from Microsoft. Fat in terms of features, for sure, but also in size and load times. It made no sense to use an other IDE, while developing software with VB.NET/C#. But with the advent of Node.JS JavaScript, so far only known as a scripting language for web pages, outgrew itself and became a serious competitor to established languages. In 2012 Adobe came out with Brackets, a lightweight IDE for developing web applications, written with the very same tech stack: HTML, CSS and JavaScript! Based on the Chromium Embedded Framework, it felt like a normal application! Mind blowing… In 2015 there was a new kid in town: Visual Studio Code (VS Code), of all things from … Microsoft. During this time, the Redmond-based company had finally jumped on the open source bandwagon and perhaps they saw that Adobe was doing some things right on the IDE market with Brackets (but also some things wrong) and you didn’t want to miss the chance to engage the open source community. The speed with which VS Code passed other IDE’s in the developer favor was quite amazing, due to the fact that the source code was openly available on GitHub and the developers in Switzerland released a new version every damn month. What was exciting for me was the question of how long it would take for someone to make this IDE based on web technology available online, i.e. in a browser. It took until 2021…","keywords":"years industry worked fat visual studio microsoft terms features size load times made sense ide developing software vbnet˼# advent nodejs javascript scripting language web pages outgrew competitor established languages adobe brackets lightweight applications written tech stack html css based chromium embedded framework felt normal application mind blowing… kid town code things … time redmond-based company finally jumped open source bandwagon market wrong didnt miss chance engage community speed passed ides developer favor amazing due fact openly github developers switzerland released version damn month exciting question long make technology online browser 2021…","text":"For most of the years I have been in the IT industry, I have worked with the “fat” Visual Studio from Microsoft. Fat in terms of features, for sure, but also in size and load times. It made no sense to use an other IDE, while developing software with VB.NET/C#. But with the advent of Node.JS JavaScript, so far only known as a scripting language for web pages, outgrew itself and became a serious competitor to established languages. In 2012 Adobe came out with Brackets, a lightweight IDE for developing web applications, written with the very same tech stack: HTML, CSS and JavaScript! Based on the Chromium Embedded Framework, it felt like a normal application! Mind blowing… In 2015 there was a new kid in town: Visual Studio Code (VS Code), of all things from … Microsoft. During this time, the Redmond-based company had finally jumped on the open source bandwagon and perhaps they saw that Adobe was doing some things right on the IDE market with Brackets (but also some things wrong) and you didn’t want to miss the chance to engage the open source community. The speed with which VS Code passed other IDE’s in the developer favor was quite amazing, due to the fact that the source code was openly available on GitHub and the developers in Switzerland released a new version every damn month. What was exciting for me was the question of how long it would take for someone to make this IDE based on web technology available online, i.e. in a browser. It took until 2021… github.dev and Github CodespacesIn Juli 2021 GitHub announced the availability of github.dev and GitHub Codespaces. The main difference between these two solutions is that Codespaces runs the IDE within a container (VM) in the background, which enables you to run your project and … it is only available for paid plans. The main purpose of github.dev is to serve as a call target of the so-called Magic Dot, an easy way to open any repository in an editor. I blogged about this capabilty a while ago, see GitHubs Magic Dot. Really amazing! Just press the dot key on every repository and you can browse the code files. vscode.devRecently, in October 2021, Microsoft (who owns GitHub) announced another online VS Code called vscode.dev. It is practically the same IDE as github.dev, with one main difference: It is not bound to a GitHub repository, but is able to open any local project or even remote repositories from GitHub. However, it also has the same limitation that you cannot run a project, because there is no VM running in the background. But it is a really neat online editor, which runs on mobile devices too and feels absolutely like a local installed VS Code. GitpodBack in 2017 some developers from Kiel, Germany started a web-based platform called Gitpod for providing fully functional orchestrated developer environments in the web. Since at that time VS Code was not yet running in the browser, they started the project Eclipse Theia, which powers several online IDE’s until today, but switched to VS Code as the team around Erich Gamma announced remote development capabilities in late 2020. Whats special about Gitpod is, that users are able to start a browser-based instance of the IDE just by adding the address of an GitHub repository as a parameter to the URL, like https://gitpod.io#https://github.com/kristofzerbe/kiko.io. Gitpot then starts a container with the source code and shows up the IDE at a random URL like https://coffee-squirrel-htamfigy.ws-eu18.gitpod.io. This so called Workspaces can be stopped, resumed, shared and downloaded, because it is a container with everything in it you need to run. Really amazing! Other Monaco-driven IDE’sVS Code actually consists of two parts: the platform itself, called Code-OSS, and the code editor Monaco included in it, which is also available as a separate project and used by other web-based IDE’s, like the following… StackblitzThe IDE on stackblitz.com is mainly useful for web frontend developers. You can easily create Angular, React, Vue, Next.js, Nuxt or even plain JavaScript or static HTML projects and connect them to an new repository on GitHub. But … you can’t load existing projects from GitHub into Stackblitz on the fly, you have to import them. What makes Stackblitz very comfortable is that it runs your frontend directly on their servers and gives it to you in a browser-like preview window via a random Url like https://web-platform-ywqj4s.stackblitz.io, you can open up in a separate browser also. CodeSandboxCodeSandbox works similar to Stackblitz, but offers some more features, like deployment to Vercel, Netlify or GitHub Pages and a test runner. Also, you have full control over the sandbox that runs the preview of your project and the ability to invite other developers or visitors to the project, which makes it perfect for online coding seminars e.g. classrooms.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"}]},{"title":"Croatian Presets for Lightroom","subtitle":null,"series":"Lightroom Presets","date":"2021-10-16","updated":"2021-10-16","path":"post/Croatian-Presets-for-Lightroom/","permalink":"https://kiko.io/post/Croatian-Presets-for-Lightroom/","excerpt":"Staying in Croatia is always a joy, in summer but also in winter. I’m on this side of the Adriatic sea almost every year. In summer you have a pleasant heat, inviting you to swim, and sometimes too many tourists. In winter you have the magic light and space to enjoy the country. Over the past years a have created some presets to bring my images, shot in Croatia, to a next level of beauty and I want to share them with you in this post.","keywords":"staying croatia joy summer winter im side adriatic sea year pleasant heat inviting swim tourists magic light space enjoy country past years created presets bring images shot level beauty share post","text":"Staying in Croatia is always a joy, in summer but also in winter. I’m on this side of the Adriatic sea almost every year. In summer you have a pleasant heat, inviting you to swim, and sometimes too many tourists. In winter you have the magic light and space to enjoy the country. Over the past years a have created some presets to bring my images, shot in Croatia, to a next level of beauty and I want to share them with you in this post. Croatian Warm SeaThe sea in Croatia is amazing clear because almost the entire coast is made of stone instead of sand. There is hardly any algae or other things that cloud the wonderful green-blue water. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-3eoql5\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Croatian Warm Sea.xmp Croatian BrightnessYes, I’m guilty: I love colors and contrast! If you do also, his preset is for you. It brings out any image that is too flat or dull by helping to control the brightness and contrast. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-h5554u\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Croatian Brightness.xmp Croatian Winter DramaA drive along the coast in the morning or evening hours makes you want to stop all the time because one panorama is more beautiful than the next. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-tbj84w\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Croatian Winter Drama.xmp Croatian Winter CityThe walls of Split have this wonderful warm tone of the winter sun and this preset brings this out particularly well. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-see498\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Croatian Winter City.xmp Croatian Winter SunsetThe sunset in Croatia is special for me, because I feel these wonderful colors and my camera isn’t able to reproduce this feeling. This preset is an approximation of it. The walls of Split have this wonderful warm tone of the winter sun and this preset brings this out particularly well. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-xu14ta\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Croatian Winter Sunset.xmp","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Presets","slug":"Presets","permalink":"https://kiko.io/tags/Presets/"}]},{"title":"The Last Image Gallery...","subtitle":"... you have to deal with: Spotlight","series":"Great Finds","date":"2021-10-10","updated":"2021-10-10","path":"post/The-Last-Image-Gallery/","permalink":"https://kiko.io/post/The-Last-Image-Gallery/","excerpt":"In the last decade(s) I have seen and tried many image galleries and lightboxes for showing images or groups of images. Depending on your needs, you can choose out of trillions of solutions, for every JS framework or vanilla JS, in every flavour, size and color. With many of them, however, you reach the limits quite quickly. Be it in terms of visual adaptability, extensibility or implementation. Customization cost time and nerves, especially if the respective library has structural weaknesses. However, from today on, I don’t need to look for a suitable solution for my next project, because I found one that leaves absolutely none of my wishes unfulfillede: Spotlight by Nextapps from Berlin, Germany. To make it clear: this is not a paid advertising text or something like that. That wouldn’t make sense either, because Spotlight is Open Source (Apache 2.0 License) and its code is availabel at GitHub. I’m just thrilled with the work of the developers.","keywords":"decades image galleries lightboxes showing images groups depending choose trillions solutions js framework vanilla flavour size color reach limits quickly terms visual adaptability extensibility implementation customization cost time nerves respective library structural weaknesses today dont suitable solution project found leaves absolutely wishes unfulfillede spotlight nextapps berlin germany make clear paid advertising text wouldnt sense open source apache license code availabel github im thrilled work developers","text":"In the last decade(s) I have seen and tried many image galleries and lightboxes for showing images or groups of images. Depending on your needs, you can choose out of trillions of solutions, for every JS framework or vanilla JS, in every flavour, size and color. With many of them, however, you reach the limits quite quickly. Be it in terms of visual adaptability, extensibility or implementation. Customization cost time and nerves, especially if the respective library has structural weaknesses. However, from today on, I don’t need to look for a suitable solution for my next project, because I found one that leaves absolutely none of my wishes unfulfillede: Spotlight by Nextapps from Berlin, Germany. To make it clear: this is not a paid advertising text or something like that. That wouldn’t make sense either, because Spotlight is Open Source (Apache 2.0 License) and its code is availabel at GitHub. I’m just thrilled with the work of the developers. The first step on implementing every image gallery solution is the installation. Spotlight has different options, but the easiest one is to download the bundled version, which includes both the JS files and the image (icon) and CSS files. For the non-bundles files, you can choose the minified versions or even the original ES6 and LESS files. There is a NPM package also. For the implementation you can either choose a declarative way using the spotlight class, where the shown thumb images to click on needs to have a wrapper… 123456<a class="spotlight" href="img1.jpg"> <img src="thumb1.jpg"></a><a class="spotlight" href="img2.jpg"> <img src="thumb2.jpg"></a> … or programmatically via JavaScript: 12345var gallery = [ { src: "img1.jpg" }, { src: "img2.jpg" }];Spotlight.show(gallery /*, options */); Next to the image, at the bottom left corner, a title, a description and/or a button can be displayed on a gallery slide, which is completely compatible with all types of devices. It is possible to define image groups to show separately in the declaration mode by having an extra wrapper around a bunch of image wrappers. All options how the gallery has to be shown are also declarative: 123456789101112<div class="spotlight-group" data-title="Group title"> <a class="spotlight" href="img1.jpg" data-title="This is a title"> <img src="thumb1.jpg"> </a> <a class="spotlight" href="img2.jpg" data-title="This is another title" data-description="This is a description"> <img src="thumb2.jpg"> </a></div> Spotlight has 9 built-in controls to show in the bar at the top left: Fullscreen Zoom in Zoom out Autofit Close Theme Play (Slideshow) Download … and there is a possibility to insert custom controls during initialization. If you provide several image sizes, Spotlight picks the optimal version regarding the current devices resolution, pixel ratio and bandwidth. But Spotlight is not limited to images. It can also display videos and even custom HTML fragments (DOM nodes) as slides. Through its JavaScript API you can fully remote control the gallery and it has several options to customize the appearance of the slides to every need you can imagine. The implementation is a breeze. It took me less than 10 minutes to get Spotlight working here on this blog at the photo page! I can’t think of any use case right now, that you couldn’t implement easily and quickly with this amazing library. Congrats folks … extremely well done!","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"Discoveries #13","subtitle":null,"series":"Discoveries","date":"2021-09-29","updated":"2021-09-29","path":"post/Discoveries-13/","permalink":"https://kiko.io/post/Discoveries-13/","excerpt":"This month, Discoveries is all about JavaScript-driven “components” that you can quickly and easily add to your own website to enhance it. Be it with a simple code viewer or an ingenious print function or simply to display or filter photos. Go on a journey of discovery… indiepenPanzoomguggenheim.jsLazy Loading Mosaic Tiling PluginScrollTriggerWinBox.jsPrint.jsSimple Text AnnotationsClicky Menus!Responsive Dropdown Menu (Vanilla Navbar Menu)Smooth-side-barPodtablejs","keywords":"month discoveries javascript-driven components quickly easily add website enhance simple code viewer ingenious print function simply display filter photos journey discovery… indiepenpanzoomguggenheimjslazy loading mosaic tiling pluginscrolltriggerwinboxjsprintjssimple text annotationsclicky menusresponsive dropdown menu vanilla navbar menusmooth-side-barpodtablejs","text":"This month, Discoveries is all about JavaScript-driven “components” that you can quickly and easily add to your own website to enhance it. Be it with a simple code viewer or an ingenious print function or simply to display or filter photos. Go on a journey of discovery… indiepenPanzoomguggenheim.jsLazy Loading Mosaic Tiling PluginScrollTriggerWinBox.jsPrint.jsSimple Text AnnotationsClicky Menus!Responsive Dropdown Menu (Vanilla Navbar Menu)Smooth-side-barPodtablejs indiepen by Hendrik and André from yetanother.blog https://indiepen.tech/ indiepen is a solution for showing code samples without the need of a code sharing platform, like codepen. Just reference a index.html, main.js and styles.css from wherever you want and indiepen is wrapping it with a neat viewer inside an IFrame. Panzoom by Cesar Morillas https://github.com/cmorillas/panzoom Implementing panning and zooming with JavaScript is not the easiest thing. Cesar has done all he work by creating this tiny ES6 module. It works both on a smartphone and in a desktop browser via mouse wheel. It is nearly perfect to zoom into an image to show its details. Lazy Loading Mosaic Tiling Plugin by Christopher Peloso https://github.com/cspeloso/Lazy-Loading-Mosaic-Tiling-Plugin Do you have a bunch of images you want to show in a masonry layout on your website? Whith Chris’ JS library it’s just a one-liner. It supports responsiveness and lazy-loading in tiny 132 lines of JS code and 4 CSS classes. Amazing! guggenheim.js by Will McKenzie http://oinutter.co.uk/guggenheim.js/ Another approach on showing images in a gallery is Guggenheim.js. It’s not responsive, but has sophisticated filtering options. Perfect for quickly finding a keyworded photo. ScrollTrigger by Greensock Inc. https://greensock.com/scrolltrigger/ Greensocks ScrollTrigger is unparalleled among scroll animation libraries. Based on its own GSAP library, ScrollTrigger can animate almost everthing, when the user is scolling through a website. Want to see it in action? Visit their demo page with dozens of examples… WinBox.js by Nextapps GmbH https://nextapps-de.github.io/winbox/ The guys from Nextapps have created a JavaScript library to show windows on a website. It’s window manager has features like minimize, maximize, move, resize, fullscreen and much more. Always wanted to recreate Windows 3.11? Let’s go … Print.js by Rodrigo Vieira https://printjs.crabbly.com/ Print.js is a library, that helps you printing anything, which can be shown in a browser. Primarily written to print PDF, it supports now also HTML (including forms), JSON and all sorts of images, even multiple. It formats the wanted content if needed and shows up the browsers print dialog. Pretty neat. Simple Text Annotations by Jacek Jarczok https://github.com/k-son/simple-text-annotations Some texts on websites require commenting. Jacek has developed a simple and elegant solution for such annotations, that works on any device without any dependencies. Clicky Menus! by Mark Root-Wiley https://github.com/mrwweb/clicky-menus Mark has created a one-level dropdown navigation menu, which is fully accessible, either by mouse click, touch or keyboard. It supports all Modern Browsers such as Firefox, Chrome, Edge, and even the “new IE” Safari. Responsive Dropdown Menu (Vanilla Navbar Menu) by Rizal https://github.com/therizaldev/vanilla-navbar-menu This classic responsive sidebar from Rival is beautifully solved with CSS and just a little JavaScript. See the demo … Smooth-side-bar by Pham Quang Huy https://github.com/dunbom6612/smooth-side-bar This sidebar solution from Pham Quang Huy recalls the sidebars Microsoft, Atlassian and others are using in their dashboards. You can fold/unfold them in order to show menu details or the icons only. See the demo … Podtablejs by Afuwape Sunday https://github.com/inlogicstudio/podtable Podtable is a library to make tables responsive to fit smaller devices. It shows as many columns as possible and hides all others behind a detail button to show them in a separate row if needed. It has no dependencies and varius options to customize the behaviour. See the demo …","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Uups ... empty posts","subtitle":"Problems with post asset files in Hexo and a solution approach","date":"2021-09-21","updated":"2021-09-21","path":"post/Uups-empty-posts/","permalink":"https://kiko.io/post/Uups-empty-posts/","excerpt":"A while ago I wrote about Automatic Duplicate Image Shadow and used indiepen for showing the result of my efforts. indiepen is a solution for showing code samples without the need of a code sharing platform, like codepen. Just reference a index.html, main.js and styles.css from wherever you want and indiepen is wrapping it with a neat viewer inside an IFrame. I did it quick and dirty first (sample files in a static folder) and now it was the time to do it right: place the sample files in a subfolder of the post in my Hexo-driven blog solution, in order to reference it from there AND have the possibility to call it directly via ./post/my-post/sample. The key to achive that in Hexo is the configuration option post_asset_folder: true, which generates a subfolder for all assets with the same name as the post. 12345|- _posts |- my-blog-post |- my-first-asset.jpg |- my-second-asset.jpg |- my-blog-post.md My idea regarding the indiepen files was having a subfolder for each indiepen in the post asset folder: 123456789|- _posts |- my-blog-post |- my-first-asset.jpg |- my-second-asset.jpgNEW |- sampleNEW |- index.htmlNEW |- main.jsNEW |- styles.css |- my-blog-post.md Run hexo generate, check that the indiepen was showing up properly and I thought I was done. Wrong … after commiting my changes to Github, where my blog is living, and checking my RSS feed a while after, I saw this: Three empty posts…!?","keywords":"ago wrote automatic duplicate image shadow indiepen showing result efforts solution code samples sharing platform codepen reference indexhtml mainjs stylescss wrapping neat viewer inside iframe quick dirty sample files static folder time place subfolder post hexo-driven blog order possibility call directly /post/my-post/sample key achive hexo configuration option post_asset_folder true generates assets 12345|- _posts |- my-blog-post my-first-assetjpg my-second-assetjpg my-blog-postmd idea asset 123456789|- my-second-assetjpgnew samplenew indexhtmlnew mainjsnew run generate check properly thought wrong … commiting github living checking rss feed empty posts…","text":"A while ago I wrote about Automatic Duplicate Image Shadow and used indiepen for showing the result of my efforts. indiepen is a solution for showing code samples without the need of a code sharing platform, like codepen. Just reference a index.html, main.js and styles.css from wherever you want and indiepen is wrapping it with a neat viewer inside an IFrame. I did it quick and dirty first (sample files in a static folder) and now it was the time to do it right: place the sample files in a subfolder of the post in my Hexo-driven blog solution, in order to reference it from there AND have the possibility to call it directly via ./post/my-post/sample. The key to achive that in Hexo is the configuration option post_asset_folder: true, which generates a subfolder for all assets with the same name as the post. 12345|- _posts |- my-blog-post |- my-first-asset.jpg |- my-second-asset.jpg |- my-blog-post.md My idea regarding the indiepen files was having a subfolder for each indiepen in the post asset folder: 123456789|- _posts |- my-blog-post |- my-first-asset.jpg |- my-second-asset.jpgNEW |- sampleNEW |- index.htmlNEW |- main.jsNEW |- styles.css |- my-blog-post.md Run hexo generate, check that the indiepen was showing up properly and I thought I was done. Wrong … after commiting my changes to Github, where my blog is living, and checking my RSS feed a while after, I saw this: Three empty posts…!? The indiepen files in the asset subfolder were treated like posts by Hexo by no obvious reason! A short research on the web lead me to a Github issue called asset files were rendered when post_asset_folder set to true from 2015, with several requests to fix the bug. Since I’m little familiar with Hexo’s processors, I saw two ways to fix the problem: Write a plugin like Hexo Hide Posts to filter out unwanted “posts” after processing Try to find the problem in Hexo’s source code, to avoid these files to be treated as posts a priori After a little searching around in Hexo’s source code, I was able to identify the location where the problem was. In the post processor: ./node_modules/hexo/lib/plugins/processor/post.js (shortened)1234567891011121314151617181920212223242526272829303132333435363738module.exports = ctx => { ... return { pattern: new Pattern(path => { if (isTmpFile(path)) return; let result; if (path.startsWith(postDir)) { result = { published: true, path: path.substring(postDir.length) }; } else if (path.startsWith(draftDir)) { result = { published: false, path: path.substring(draftDir.length) }; } if (!result || isHiddenFile(result.path)) return; result.renderable = ctx.render.isRenderable(path) && !isMatch(path, ctx.config.skip_render); return result; }), process: function postProcessor(file) { if (file.params.renderable) { return processPost(file); } else if (ctx.config.post_asset_folder) { return processAsset(file); } } };};... In the very beginning of processing posts, the code iterates over all files and folders in the _posts folder and creates a param object for each by using the pattern. This object has an attribute named renderable, which is used by the process method later on, to decide if it has to be rendered or not. But … the determination of renderable is based solely on wether a file has a renderer (isRenderable) or if it is listed in the skip_render configuration. See line 24 in the sample code. As HTML, JS and CSS files naturally have a renderer in Hexo, they will treated as posts! Alleviation Of The ProblemIn this early stage of code execution, only files and folders are listed. There is no reference to a post or something like that, in order to find out if a certain file belongs to a post as an asset. Examination of the path will fail, because it is possible in Hexo to have a folder hierarchy under _posts. The only way to approach the problem solution is by making assumptions: If the user has activated the configuration post_asset_folder, he wants to have asset files in a folder named after the post If the user has configured a certain file name for new posts in new_post_name (for example :title.md), it is not to be assumed, that he will create asset files with the same file extension. Important here is, that it is not an option to hide the files in some way, because then they won’t appear in the website. They have to be marked as process it, but don’t render it, by bending the renderable attribute. These considerations led to a small extension of the above code below line 24: ./node_modules/hexo/lib/plugins/processor/post.js (shortened)12345678910...result.renderable = ctx.render.isRenderable(path) && !isMatch(path, ctx.config.skip_render);//if post_asset_folder is set, restrict renderable files to default file extension if (result.renderable && ctx.config.post_asset_folder) { result.renderable = (extname(ctx.config.new_post_name) === extname(path));}... This is what I pushed as a Pull Request to the Hexo team today. I know, it not ideal, but maybe they accept my approach and ship the fix with the next version.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"Photo Workflow Re-Thought","subtitle":null,"series":"Step By Step","date":"2021-09-12","updated":"2021-09-12","path":"post/Photo-Workflow-Re-Thought/","permalink":"https://kiko.io/post/Photo-Workflow-Re-Thought/","excerpt":"To be honest: many of my posts here on kiko.io are written just for me. To internalize things by writing them down and to give my future me the chance to look up something I did in the past. So is this post. Future me: Don’t forget the following! I while ago I was on a trip, shooting a lot of photographs and on my way back home I had three 32GB SD cards full with great photos. I was working with my old Nikon D7000, which has 2 card slots and I just took them out, when one of the cards was full. Worked fine for several years … but after this trip, one of the cards, full with RAW files and wonderful photos, w-a-s N-O-T r-e-a-d-a-b-l-e a-n-y-m-o-r-e … f***! I saved myself the backup and now had to suffer over my carelessness. I thought about some fancy and expensive backup solutions for professional photographers, but realized after a while, that I already had the equipment to achieve everything I needed and I could even use it to improve my general workflow. In this post, I want to show you, what my workflow looks like today and how yours might benefit from my mistake.","keywords":"honest posts kikoio written internalize things writing give future chance past post dont forget ago trip shooting lot photographs back home 32gb sd cards full great photos working nikon d7000 card slots worked fine years … raw files wonderful w-a-s   n-o-t   r-e-a-d-a-b-l-e   a-n-y-m-o-r-e f*** saved backup suffer carelessness thought fancy expensive solutions professional photographers realized equipment achieve needed improve general workflow show today benefit mistake","text":"To be honest: many of my posts here on kiko.io are written just for me. To internalize things by writing them down and to give my future me the chance to look up something I did in the past. So is this post. Future me: Don’t forget the following! I while ago I was on a trip, shooting a lot of photographs and on my way back home I had three 32GB SD cards full with great photos. I was working with my old Nikon D7000, which has 2 card slots and I just took them out, when one of the cards was full. Worked fine for several years … but after this trip, one of the cards, full with RAW files and wonderful photos, w-a-s N-O-T r-e-a-d-a-b-l-e a-n-y-m-o-r-e … f***! I saved myself the backup and now had to suffer over my carelessness. I thought about some fancy and expensive backup solutions for professional photographers, but realized after a while, that I already had the equipment to achieve everything I needed and I could even use it to improve my general workflow. In this post, I want to show you, what my workflow looks like today and how yours might benefit from my mistake. PrerequisitesHardware Nikon D500amazon.com Sony Professional XQD G Series 64GB Memory Cardamazon.com Prograde Digital SD UHS-II 64GB Cardamazon.com The Nikon D500 has 2 different card slots: XQD and SD, where the SD is configured in the camera as backup medium. It is important to use a SD card that is as big and as fast, in terms if writing speed, as the used XQD card! Lenovo Yoga Smart Tab (Android)amazon.com This tablet has a Micro SD card slot for extending the internal memory up to 128 GB (inofficially up to 2TB) and a USB type C socket. Rocketek Type C XQD/SD Card Readeramazon.com This card reader has two slots for XQD and SD and a USB Type C connector with a short cable. … and any Windows 10 machine Services Dropboxdropbox.com Apps for Android (Tablet) Solid Explorer File Managerplay.google.com Photo Mate R3play.google.com Autosync for Dropbox - Dropsyncplay.google.com Apps for Windows 10 Adobe Lightroom Classic (CC)adobe.com The Workflow In GeneralThe aim of the workflow is to transfer the photos from the camera to other hardware on the one hand and to cloud-based storage on the other, from where they can then be easily moved to the actual storage medium at home. In the end, always 2 different backups will exist on different media. When traveling, it is important to always have the tablet and the card reader with you, in addition to the camera and some spare SD cards. Step 1Whenever it is necessary or advisable, take both cards out of the camera and put the XQD card into the card reader and connect the latter to the tablet. Just put the SD card away, as we will use a spare card afterwards. Open up Solid Explorer on the tablet… Copy the complete folder DCIM/101ND500 (where the photos are stored) to a separate folder called RAW on the disk (SD card extension) of the tablet. Step 2Rename the new folder on the disk to something appropriate:(Screenshot says Rename a file, but that’s a bug in the app) Step 3Insert the XQD card and a spare SD card back into the camera and wipe them both via the built-in menu. Your backup is now on the orginal SD cards and your tablet. Step 4 (optional)If you have time and leisure, you can use Photo Mate R3 to review and rate your images. Don’t delete any photos in this step!Just mark photos with RED, which can be removed afterwards, because the app is sometimes not fast enough to delete the right photo, when you has opened the next one! Step 5Open up Dropsync on the tablet and set up a new sync profile, if you haven’t done so already: The remote folder is a folder called RAW in your Dropbox and the local folder is the RAW folder on your disk. Sync method should be Upload then delete, because you don’t need the second backup on your tablet, after the photos are transfered to Dropbox. Run the synchronization: Your backup is now on the original SD cards and Dropbox. Step 6After you have enjoyed your trip and be back home, move the photo folder from your Dropbox to your local storage, which of course should also have a backup of some kind: Your backup is now on the original SD cards and your local storage. Step 7If you’ve already rated your images along the way, Photo Mate R3 has already stored that metadata in an XMP sidecar file, that is compatible with Lightroom: Open up Lightroom and import the folder from your local storage: Step 8In the Lightroom library, filter all photos with the RED flag and set them as REJECTED: Step 9Delete all photos marked as REJECTED: Step 10If you are sure that your local backup was done once, you can safely wipe the original SD cards, in order to use them for the next time.","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Workflow","slug":"Workflow","permalink":"https://kiko.io/tags/Workflow/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"Application-Specific Links on Windows 10","subtitle":"What works and what we still have to wait for","date":"2021-09-03","updated":"2021-09-03","path":"post/Application-Specific-Links-on-Windows-10/","permalink":"https://kiko.io/post/Application-Specific-Links-on-Windows-10/","excerpt":"While reading the Chris Coyier’s post Application-Specific Links the other day, I realized what has been bugging me for a long time now: a proper solution for openening a certain URL in a modern Web App. Since the beginning of the digital age (feels like that), we have files associated to a certain application, installed on our machine, regardless if its running Windows, iOS, OS/2 or whatever. We have learned that well and no one questions it … but … the IT world keeps on turning and today we are not only talking about files, but about links. Many modern applications are written with Web technologies, thanks to cross platform frameworks like Electron. Some of them are real apps for working on things, like the famous editor Visual Studio Code, and some are mirroring their online services in a desktop app only, like Slack or Notion. However, the latter have the problem how to deal with links to their online services. When a user is sent a link and has become accustomed to using the desktop app, it won’t open in the app as he clicks on it, but in his default browser. The question is, how to associate not only files, but links with certain desktop apps?","keywords":"reading chris coyiers post application-specific links day realized bugging long time proper solution openening url modern web app beginning digital age feels files application installed machine running windows ios os˲ learned questions … world turning today talking applications written technologies cross platform frameworks electron real apps working things famous editor visual studio code mirroring online services desktop slack notion problem deal user link accustomed wont open clicks default browser question associate","text":"While reading the Chris Coyier’s post Application-Specific Links the other day, I realized what has been bugging me for a long time now: a proper solution for openening a certain URL in a modern Web App. Since the beginning of the digital age (feels like that), we have files associated to a certain application, installed on our machine, regardless if its running Windows, iOS, OS/2 or whatever. We have learned that well and no one questions it … but … the IT world keeps on turning and today we are not only talking about files, but about links. Many modern applications are written with Web technologies, thanks to cross platform frameworks like Electron. Some of them are real apps for working on things, like the famous editor Visual Studio Code, and some are mirroring their online services in a desktop app only, like Slack or Notion. However, the latter have the problem how to deal with links to their online services. When a user is sent a link and has become accustomed to using the desktop app, it won’t open in the app as he clicks on it, but in his default browser. The question is, how to associate not only files, but links with certain desktop apps? Executables and URL SchemesTriggered by Notions announcement of using a dedicated link protocol notion:// to open up Notion links directly in the Notion App, Chris wrote about the possibilities an a Mac to associate certain URL schemes with a Web App in general. His choice fell on Choosy, an app which hooks into to the process on opening links by beeing the systems default browser and forwarding the URL to configured applications. Fair enough, but it’s a tool for Mac. However, it was not particularly difficult to find equivalent programs for Windows 10, my preferred OS: Browser Selector Browser Select Browser Picker Browser Choose 2 … name yours Interestingly, Microsoft already has something similar built into Windows, but prevents users from messing with it: The feature is targeted from UWP applications only, therefore the developer has to implement it by extending the app manifest: Web-to-App Linking with AppUriHandlers. What Chris’ approach and all similar Windows programs have in common is, that they are based on executables, EXE files and yes, the cross-platform apps mentioned above are executables, but for me this is too short. What about Chromium based Progressive Web Apps (PWA)? Since the Chromium team has introduced the possibility to add a website as desktop shortcut and start it in a new window, without any browser specific toolbars and other stuff, it became very popular, especially if this website was implemented by the developer as a PWA.It does not matter whether all PWA features, such as offline mode or similar, are available. The mere fact of being able to use an online service in its own window is a gain. But such an app has no specific executable! It will be executed by the browser with some parameters itself. The question is, how to associate a certain URL scheme or a custom protocol with these web apps? PWA’s on WindowsSome basics about how Chromium based browsers are dealing with this feature: As you click on Create Shortcut… in the More Tools menu in Chrome or Edge and confirm the following dialog (or install the PWA), a new folder in %LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Web Applications will be created. Its name starts with _crx_ and is followed by a random string (so called app-id), f.e. ffokdlainpppngbbhdcobaocmbobgdii. In this folder the Favicon of the website is stored. If it is a real PWA and has a Web App Manifest, you will find all resources defined there in another subfolder named by the app-id below %LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Web Applications\\Manifest Resources. The paths differ depending on which browser profile is active. In this case ‘default’. The desktop shortcut, which will be created, points to an executable named chrome_proxy.exe or msedge_proxy.exe. These files are scaled-down versions of the browser executable itself. Its main purpose is to bypass a Windows error as described in its source code, in order to create a correct shortcut. The important part of the shortcut are the calling parameters: --app-id - the random ID generated by the browser --profile-directory - the browser profile to use; mostly ‘default’ If you are interested, Chromium has dozens of parameters, some to change the behaviour and others only for debugging purposes. Peter Beverloo provides an automatic generated List of all Chromium Command Line Switches. … a bit deeper, just for funThe following has nothing to do with the actual problem and doesn’t help in any way, but it was interesting to dig a bit deeper into the operating principles of Chromium. As we saw that the browser creates a unique ID for the new shortcut, we find all needed data referenced in the file %LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\preferences. It has no extension, but it is a JSON file. Two sections are relevant in this file: browser.app_window_placement._crx_<app-id> Holds the information how the window is displayed on the desktop (metrics). preferences > browser.app_window_placement1234567891011"_crx_ffokdlainpppngbbhdcobaocmbobgdii": { "bottom": 889, "left": 161, "maximized": false, "right": 1610, "top": 56, "work_area_bottom": 920, "work_area_left": 0, "work_area_right": 1707, "work_area_top": 0} extensions.settings.<app-id> Holds every other information about the app, including the complete manifest und the URL to show. preferences > extensions.settings (shortened)12345678910111213141516171819202122232425262728293031323334353637383940414243444546"ffokdlainpppngbbhdcobaocmbobgdii": { "active_bit": false, "active_permissions": { "api": [], "manifest_permissions": [] }, "app_launcher_ordinal": "zzo", "commands": {}, "content_settings": [], "creation_flags": 17, "events": [], "from_bookmark": true, "from_webstore": false, "granted_permissions": { "api": [], "manifest_permissions": [] }, "incognito_content_settings": [], "incognito_preferences": {}, "install_time": "13266055455495996", "launchType": 3, "locallyInstalled": true, "location": 1, "manifest": { "app": { "display_mode": "browser", "icon_color": "#186AA5", "launch": { "web_url": "https://trello.com" }, "linked_icons": [ ... ] }, "description": "Organize anything, together. Trello is a collaboration tool that organizes your projects into boards. In one glance, know what's being worked on, who's working on what, and where something is in a process.", "icons": { ... }, "key": "", "name": "Trello", "version": "2021.5.21.20218" }, "page_ordinal": "y", "path": "ffokdlainpppngbbhdcobaocmbobgdii\\\\2021.5.21.20218_0", "preferences": {}, "regular_only_preferences": {}, "state": 1, "was_installed_by_default": false, "was_installed_by_oem": false}, It is possible to edit the preferences manually, but not advisable, because its the backbone of your browser profile and you have to restart all browser instances after changes. Registering Protocol Handlers via JavaScriptThere are native protocol handlers in every OS. The most famous is mailto:. A click on such a link opens the default mail editor. Since 2006 the W3C discusses an extension called registerProtocolHandler, every web app could implement to tell the OS, that it is responsible for a certain protocol. 123navigator.registerProtocolHandler("web+myfancyapp", "https://my-fancy-web-app.com/?url=%s", "My Fancy App Handler"); If this registrations as the user enters the site for the first time, he will be asked by the browser for allowance, he has to confirm to make it work. As the developer implements the links, he has to write it like that: 1<a href="web+myfancyapp:settings">Go To Settings</a> The registration and a click on such a link causes the browser to request https://my-fancy-web-app.com/?url=settings. For understandable reasons some protocols are blacklisted, like mailto, irc, tel and others. Custom protocol schemes has to have the prefix web+, in order to avoid interference with the standardized protocols. As of May 2021, it is part of the capabilities project and is in development currently. Nice … but doesn’t helps in solving our problem in the first place, because we can’t yet break out of the browser with it. We need something to tell the browser that an installed PWA is responsible for this protocol… Manifest Protocol HandlersThere is no official W3C proposal, but experiments around the Chromium team to extend the PWA’s manifest file like that: web.manifest123456"protocol_handlers": [ { "protocol": "web+myfancyapp", "url": "/?url=%s" }] This could be the bridge we need to bind a PWA to a certain protocol, in order to open up the registered browser app, instead of a new tab. But its is just an experiment. Manifest URL HandlersAnother approach on connecting certain links to a PWA is similar to the manifests protocol_handlers but for URL schemes. No need for registering a protocol … browser just parse the URL and show up my app: web.manifest12345678"url_handlers" : [ { "origin": "https://my-fancy-app.com" }, { "origin": "https://*.my-fancy-app.com" }] Simple and straightforward. There is a Chrome Platform Page for this feature and the current status is Origin Trial for version 93! To try it out, you can enable #enable-desktop-pwas-url-handling in Edge and Chrome: But … it is also not even on the way to an official proposal :| ConclusionAs of today there is no solution for Windows users to associate links with the bunch of different applicationson windows, as we are used to from the files. But there is a special technique regarding web apps at the horizon, zo ease this pain, even if it depends on the app developers. Maybe Microsoft should think about their approach of Apps for Websites, in order to make it possible for users to extend that. The cherry on top would be then, if they decide to integrate the upcoming Protocol and URL Handlers, defined by the developers, right into this tool. More Info whatwg.org: HTML - Living StandardMDB Web Docs: Web-based protocol handlersThomas Steiner (web.dev): URL protocol handler registration for PWAsMatt West: Registering Protocol Handlers to Intercept Special LinksGyuyoung: What is navigator.registerProtocolHandler?GitHub Issue on W3C/Manifest: Add manifest option for PWAs to be registered as scheme/protocol handlersMDN Web Docs: protocol_handlersMicrosoft Docs: Experimental features in Progressive Web Apps (PWAs)GitHub MicrosoftEdge/MSEdgeExplainers: URL Protocol Handler Registration for PWAs","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"},{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"PWA","slug":"PWA","permalink":"https://kiko.io/tags/PWA/"}]},{"title":"Pattern for dynamic Hexo pages","subtitle":"Set up pages with dynamic data easily","date":"2021-08-25","updated":"2021-08-25","path":"post/Pattern-for-dynamic-Hexo-pages/","permalink":"https://kiko.io/post/Pattern-for-dynamic-Hexo-pages/","excerpt":"Hexo is a great SSG platform for blogging. Just write your Markdown beneath some Frontmatter meta data, run hexo generate and publish the results to a web server. But at some point you may want to process different data from internal or external sources and integrate it into your blog. Hexo doesn’t support this out of the box, but has a powerful feature called Generator, which helps you to achieve your goal. The following is a sample and pattern of how to implement this. The starting point of my example is the requirement to display several elements of the same type on a dynamic page, but you can of course adapt the example according to your needs.","keywords":"hexo great ssg platform blogging write markdown beneath frontmatter meta data run generate publish results web server point process internal external sources integrate blog doesnt support box powerful feature called generator helps achieve goal sample pattern implement starting requirement display elements type dynamic page adapt","text":"Hexo is a great SSG platform for blogging. Just write your Markdown beneath some Frontmatter meta data, run hexo generate and publish the results to a web server. But at some point you may want to process different data from internal or external sources and integrate it into your blog. Hexo doesn’t support this out of the box, but has a powerful feature called Generator, which helps you to achieve your goal. The following is a sample and pattern of how to implement this. The starting point of my example is the requirement to display several elements of the same type on a dynamic page, but you can of course adapt the example according to your needs. The PageFirst of all we need a Hexo page for the meta data and for some text we want to show at the beginning of the page to describe what is shown below. In order not to interfer the classic post generation, we create a new folder in Hexo’s source folder called _dynamic, where we place the MD file for the new dynamic page. The underscore in the name of the folder _dynamic is important, because Hexo doesn’t touch subfolders in source with this starting character while generating the site. Without, it would be treated as a normal page. ./source/_dynamic/my-special-page.md123456789---title: My Titlesubtitle: My Subtitledate: 2021-08-24 19:24:00mySpecialProperty: "Lorem ipsum dolor sit amet"---Some text to show above the dynamic created content... The Frontmatter data has no limits. You can add as much properties as you like. The Layout TemplateHexo works with EJS layout files. Whenever a page should be created, the calling method has to define which layout file should be used. Therefor we create a special EJS file for our dynamic page and place it in the layout folder. ./themes/<your-theme>/layout/my-special-layout.ejs1234567891011121314151617<h1 class="page-title"><%= page.title %></h1><h2 class="page-subtitle"><%= page.subtitle %></h2><div class="page-content"> <%- page.content %> <div class="view grid"> <% for(var i=0; i < page.items.length; i++) { %> <%- partial('_partial/my-special-item', { item: page.items[i] }) %> <% } %> </div> </div><script> // Extend the page with JavaScript ...</script> On generating the page later on, an object called page will be used to process the EJS, with all information we want to show on the page, like content (text of the Markdown file) and all meta data from the Frontmatter (title, subtitle and so on). Special attention is paid to items, because this property holds the list of all items we want to show on the page. Each object in this list have multiple custom properties you define, when you are assemble the data to show in the generator, but more about that later. In the script tag you can write JavaScript to interact with your data on the page, like filtering stuff or other things the user should can do with your data. The Partial Template For ItemsIn order not to blow up the layout EJS, it is advisable to separate the template for the item itself to an extra file, referenced in the layout file (_partial/my-special-item) as a Partial. ./themes/<your-theme>/layout/_partial/my-special-item.ejs123<div class="my-item"> <!-- Definition of an item --></div> The layout EJS will iterate over the list of items in the FOR loop and handover one of these object to the Partial to render. The GeneratorAs we have made our preparations, we are now able to implement our special generator itself. Hexo uses so called Generators to “produce” the HTML for your site and you can add your own by using the register method. The result of a generator has to be an object with at least three properties: data - Data object with all necessary information to process the given layout EJS path - The path where the HTML file should be created layout - The layout EJS file, which should be processed ./themes/<your-theme>/scripts/my-special-generator.js123456789101112131415161718hexo.extend.generator.register("my-special-generator", async function(locals) { // INSTANTIATE A NEW DATA OBJECT let page = {}; page.name = "my-special-page"; // Do something to get content and data for the page // INSTANTIATE THE RESULT OBJECT let result = { data: page, path: path.join(page.name, "index.html"), layout: "my-special-layout" } // RETURN THE RESULT return result; }); In this basic structure, you will find again the layout file, we created earlier. The output will be rendered in an HTML file called index.html at the subfolder my-special-page, as we take the name of the page for it, as we did it for the name of the source MD file. The next thing we have to implement here, is the content of the source MD file. The parameter locals gives you access to the site variables, with all information about the site and its pages, posts, categories and tags! The first 4 lines in the following script are the references to some helpers we need. Please be sure, that you install them first via npm install. ./themes/<your-theme>/scripts/my-special-generator.js12345678910111213141516171819202122232425262728293031323334353637383940414243const log = require('hexo-log')({ debug: false, silent: false });const path = require('path');const fs = require('hexo-fs');const front = require('hexo-front-matter');hexo.extend.generator.register("my-special-generator", async function(locals) { // GET REFERENCE TO HEXO'S CONFIGURATION let config = this.config; // SHOW MESSAGE ON GENERATING log.info("Processing items for dynamic page"); let page = {}; page.name = "my-special-page"; // GET THE PATH TO THE SOURCE FILE const mdSource = path.join(config.source_dir, "_dynamic", page.name + ".md"); // GET THE CONTENT OF THE SOURCE FILE const md = fs.readFileSync(mdSource); // PARSE THE FRONTMATTER OF THE SOURCE FILE let fm = front.parse(md); // ADD THE FRONTMATTER TO THE DATA OBJECT page = {...page, ...fm}; // CONVERT MARKDOWN CONTENT OF THE SOURCE FILE INTO HTML page.content = hexo.render.renderSync({ text: page._content, engine: 'markdown' }); // Do something to get items data for the page //page.items = []; //... let result = { data: page, path: path.join(page.name, "index.html"), layout: "my-special-layout" } return result; }); The only thing missing now, is your implemention of filling page.items with a list of objects you want to show on the page. There are no limits to your imagination. Get data from external API’s or process JSON data, stored in the data folder … or whatever you prefer. A live example of this approach are the pages TINY TOOLS (processing data from the Trello API) and PHOTOS here on this blog.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"Visualize the codebase of your GitHub repo","subtitle":"Try GitHub Next's repo-vizualizer action for generating a diagram of your codebase","date":"2021-08-21","updated":"2021-08-21","path":"post/Visualize-the-codebase-of-your-GitHub-repo/","permalink":"https://kiko.io/post/Visualize-the-codebase-of-your-GitHub-repo/","excerpt":"Beginning of the month, Amelia Wattenberger of GitHub Next has published a project to create a SVG visualization of a GitHub repository’s codebase. On the project page Visualizing a codebase, she talks about the advantages of code vizualization in terms of a better overview and comparability of code … and I loved it at first sight, because I’m an absolute visual person. But her attempt was not only to show us what’s possible (static SVG files and even interactive apps for code browsing, filtering and comparing), but give us the possibility to create our own codebase diagrams as SVG automatically, whenever we commit our code, by running a GitHub Action, she and her team has developed … the Repo Vizualizer","keywords":"beginning month amelia wattenberger github published project create svg visualization repositorys codebase page visualizing talks advantages code vizualization terms overview comparability … loved sight im absolute visual person attempt show whats static files interactive apps browsing filtering comparing give possibility diagrams automatically commit running action team developed repo vizualizer","text":"Beginning of the month, Amelia Wattenberger of GitHub Next has published a project to create a SVG visualization of a GitHub repository’s codebase. On the project page Visualizing a codebase, she talks about the advantages of code vizualization in terms of a better overview and comparability of code … and I loved it at first sight, because I’m an absolute visual person. But her attempt was not only to show us what’s possible (static SVG files and even interactive apps for code browsing, filtering and comparing), but give us the possibility to create our own codebase diagrams as SVG automatically, whenever we commit our code, by running a GitHub Action, she and her team has developed … the Repo Vizualizer Actually, her instructions are quite simple to implement, but the devil is in the details and I would like to show you what you may need for this. The goal is to prepare every project hosted on GitHub with instructions to run the Repo Visualizer after every commit to create or update a SVG file in the project, we can use in the README or via hotlinking in every other web page. Let’s start with my setup: Windows 10 Visual Studio Code a bunch of tiny projects hosted on GitHub Project IntegrationGitHub actions are configured via YAML files in the folder .github\\workflows. Therefore, just take Amelias demo file diagram.yaml and copy it to this folder. .github\\workflows\\diagram.yml12345678910111213141516name: Create Vizualizing Codebase Diagramon: workflow_dispatch: {} push: branches: - mainjobs: get_data: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@master - name: Update diagram uses: githubocto/repo-visualizer@main with: excluded_paths: "ignore,.github" You don’t have to change anything in this file, except the name if you want to. It is clear to run correctly. Prepare WindowsIn case you get the following error message on pushing the new file in your project to GitHub … 1refusing to allow an OAuth App to create or update workflow .github/workflows/diagram.yml without workflow scope … you have a problem with your OAuth Token the Git Credential Manager created for you, while installing Git for Windows. This token doesn’t include the permission to update GitHub action workflows. A workaround is, to create a Personal Access Token on Github WITH this permission … … and replace the existing token in the Windows Credential Manager (in German the wonderful word “Anmeldeinformationsverwaltung”): ParametersThere are a few interesting parameters to place within the with section, that allow you to customize the output of the graph to your needs: output_file - The name and relative path to the SVG file to generate. Default is ./diagram.svg root_path - The root path of the code to be vizualized excluded_paths - Folders to exclude from visualizing (as in the demo YAML) excluded_globs - Files to exclude from visualizing in micromatch syntax commit_message - After the action has created the diagram, it will be commited with this custom message After pushing your changes, you can watch the action run in the ACTIONS tab in GitHub: The READMEAs the SVG is part of yor project, you can use it easily in your repos README: README.md123## Codebase VizualizationFor an inactive diagram, please visit [Repo Visualization App](https://octo-repo-visualization.vercel.app/?repo=kristofzerbe%2Fhexo-generator-anything)...![Visualization of the codebase](./DIAGRAM.svg) This SVG is not interactive as Amelias Repo Visualization React app is, but it is a good overview of a repos codebase und it looks really good.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"GitHubs Magic Dot","subtitle":"New experience in code editing","date":"2021-08-20","updated":"2021-08-20","path":"post/GitHubs-Magic-Dot/","permalink":"https://kiko.io/post/GitHubs-Magic-Dot/","excerpt":"GitHub has released the long awaited solution für code editing in the web today, or more precisely, the two new solutions: CodeSpaces and github.dev. As Brigit Murtaugh and Allison Weins pointed out in their presentation at the live stream on Youtube (VS Code anywhere: GitHub Codespaces and github.dev), both solutions are based on the codebase of Visual Studio Code, but have different approaches and target groups. Where Codespaces is an online editor with computing capabilities (realized by running a VM in the backend) and is only available für paid plans, github.dev is a free online editor to change files in your GitHub repo as you do it in your local VSCode, but without running and debugging capabilities.","keywords":"github released long awaited solution für code editing web today precisely solutions codespaces githubdev brigit murtaugh allison weins pointed presentation live stream youtube based codebase visual studio approaches target groups online editor computing capabilities realized running vm backend paid plans free change files repo local vscode debugging","text":"GitHub has released the long awaited solution für code editing in the web today, or more precisely, the two new solutions: CodeSpaces and github.dev. As Brigit Murtaugh and Allison Weins pointed out in their presentation at the live stream on Youtube (VS Code anywhere: GitHub Codespaces and github.dev), both solutions are based on the codebase of Visual Studio Code, but have different approaches and target groups. Where Codespaces is an online editor with computing capabilities (realized by running a VM in the backend) and is only available für paid plans, github.dev is a free online editor to change files in your GitHub repo as you do it in your local VSCode, but without running and debugging capabilities. Features like extensions, code completion, references, creating pull requests and other things we need all day in our VSCode, works also in these web editors, including settings sync! github.devThe non-computing web editor is fully integrated in GitHub itself. There is no URL github.dev, where you can load your repo. Instead, if the URL to your repo is f.e. https://github.com/myname/myrepo, just replace .com with .dev and your code is available in the editor! For people who like it even more comfortable: go to your repo and just press the DOT key. Really smart. This even works on mobile, which will definitely change my workflow…","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"}]},{"title":"Discoveries #12 - Tutorials","subtitle":"Some great articles from CSS masters","series":"Discoveries","date":"2021-07-30","updated":"2021-07-30","path":"post/Discoveries-12-Tutorials/","permalink":"https://kiko.io/post/Discoveries-12-Tutorials/","excerpt":"This months discoveries it’s all about CSS tutorials. You will find some of the best articles of profound CSS masters like Ahmad Shadeed or Josh Comeau. All tips are vital to bring your CSS to the next level. Happy learning… CSS Variables 101Full-Bleed Layout Using CSS GridA Deep Dive Into CSS Grid minmax()Create Diagonal Layouts Like It's 2020Using calc to figure out optimal line-heightDrop-Shadow: The Underrated CSS FilterA Guide to the Responsive Images Syntax in HTMLCentering in CSSHow to trigger a CSS animation on scrollCSS Scroll Snap","keywords":"months discoveries css tutorials find articles profound masters ahmad shadeed josh comeau tips vital bring level happy learning… variables 101full-bleed layout grida deep dive grid minmaxcreate diagonal layouts 2020using calc figure optimal line-heightdrop-shadow underrated filtera guide responsive images syntax htmlcentering csshow trigger animation scrollcss scroll snap","text":"This months discoveries it’s all about CSS tutorials. You will find some of the best articles of profound CSS masters like Ahmad Shadeed or Josh Comeau. All tips are vital to bring your CSS to the next level. Happy learning… CSS Variables 101Full-Bleed Layout Using CSS GridA Deep Dive Into CSS Grid minmax()Create Diagonal Layouts Like It's 2020Using calc to figure out optimal line-heightDrop-Shadow: The Underrated CSS FilterA Guide to the Responsive Images Syntax in HTMLCentering in CSSHow to trigger a CSS animation on scrollCSS Scroll Snap CSS Variables 101 by Ahmad Shadeed https://ishadeed.com/article/css-vars-101/ CSS variables (or ‘custom properties’) are a huge helper to structure your CSS code and make it more maintainable. Ahmad shows us how to use their complete power. Full-Bleed Layout Using CSS Grid by Josh Comeau https://www.joshwcomeau.com/css/full-bleed/ Today it can be tricky to layout websites for small devices like smartphones and very large desktop screens. Josh shows us how to utilize CSS Grid on a classic 3-column layout the smart way. A Deep Dive Into CSS Grid minmax() by Ahmad Shadeed https://ishadeed.com/article/css-grid-minmax/ Ahmad again. In this tutorial he brings us a deeper look into the MINMAX() function, which is not easy to understand and to use. Create Diagonal Layouts Like It's 2020 by Nils Binder https://9elements.com/blog/pure-css-diagonal-layouts/ Over 90% of all websites have an easy to understand rectangular layout, I guess. Nils shows us how to bring some disruptions into those layouts, by implementing diagonal sections, to make them less boring. Using calc to figure out optimal line-height by Jesús Ricarte https://kittygiraudel.com/2020/05/18/using-calc-to-figure-out-optimal-line-height/ Text on the web must be easy to read and one of the probably most underrated options on styling text is the line height. Jesús shows us, how to calculate the line height for an optimal reading experience. Drop-Shadow: The Underrated CSS Filter by Michelle Barker https://css-irl.info/drop-shadow-the-underrated-css-filter/ There are two shadow properties in CSS: box-shadow and drop-shadow. Michelle show us how to use the one or the other properly. A Guide to the Responsive Images Syntax in HTML by Chris Coyier https://css-tricks.com/a-guide-to-the-responsive-images-syntax-in-html/ Chris must not be missing in a list of CSS tutorials. As CSS has a bunch of tags and properties to show images in responsive layouts, he shows us how to use them effectively. Centering in CSS by Adam Argyle https://web.dev/centering-in-css/ Centering elements can be a mess, but not for Adam who shows all possibilities with PRO’s and CON’s. How to trigger a CSS animation on scroll by Nick Ciliak https://coolcssanimation.com/how-to-trigger-a-css-animation-on-scroll/ Animations, cleverly used, give a page the finishing touches. But it is important to control them. Nick shows us how to achieve this using CSS and a bit JavaScript. CSS Scroll Snap by Ahmad Shadeed https://ishadeed.com/article/css-scroll-snap/ And finally once again Ahmad. In this tutorial he deals comprehensively with CSS’s scroll-snap feature.","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"Tutorial","slug":"Tutorial","permalink":"https://kiko.io/tags/Tutorial/"}]},{"title":"Running Rollup with Gulp","subtitle":"Use Rollup.js as JavaScript bundler in your Gulp pipeline","date":"2021-07-29","updated":"2021-07-29","path":"post/Running-Rollup-with-Gulp/","permalink":"https://kiko.io/post/Running-Rollup-with-Gulp/","excerpt":"Writing an SPA (Single Page Application) in JavaScript/CSS always means to keep an eye on small files to deliver. Especially when utilizing a bunch of libraries and frameworks, bundling is some sort of a must. The offer on bundlers and task runners is large on the web: WebPack, Snowpack, Browserify, Parcel, Grunt, Gulp and “DingDong” (just replace with the hotest new shit available). But, it is not always necessary to replace your complete building pipeline, when the new “DingDong” is hyped in the media. Brave old tools like Gulp are doing their job pretty well … and you are able to integrate some more modern approaches on bundling JS, for example. I couple of months ago, while working on a private project, I became attentive to Rollup.js, a next-generation JavaScript module bundler from Rich Harris, the author of Svelte. Rollup uses the new standardized format for code modules included in the ES6 revision of JavaScript and supports Tree-Shaking, which means that it analyzes all your ES6 imports statements and bundles only the code which is used. Pretty cool … but … it is a JavaScript bundler only and there are no plugins for Gulp, my favourite task runner. In this article I will show you, how to integrate Rollup in your Gulp bundling pipeline.","keywords":"writing spa single page application javascript˼ss means eye small files deliver utilizing bunch libraries frameworks bundling sort offer bundlers task runners large web webpack snowpack browserify parcel grunt gulp dingdong replace hotest shit complete building pipeline hyped media brave tools job pretty … integrate modern approaches js couple months ago working private project attentive rollupjs next-generation javascript module bundler rich harris author svelte rollup standardized format code modules included es6 revision supports tree-shaking analyzes imports statements bundles cool plugins favourite runner article show","text":"Writing an SPA (Single Page Application) in JavaScript/CSS always means to keep an eye on small files to deliver. Especially when utilizing a bunch of libraries and frameworks, bundling is some sort of a must. The offer on bundlers and task runners is large on the web: WebPack, Snowpack, Browserify, Parcel, Grunt, Gulp and “DingDong” (just replace with the hotest new shit available). But, it is not always necessary to replace your complete building pipeline, when the new “DingDong” is hyped in the media. Brave old tools like Gulp are doing their job pretty well … and you are able to integrate some more modern approaches on bundling JS, for example. I couple of months ago, while working on a private project, I became attentive to Rollup.js, a next-generation JavaScript module bundler from Rich Harris, the author of Svelte. Rollup uses the new standardized format for code modules included in the ES6 revision of JavaScript and supports Tree-Shaking, which means that it analyzes all your ES6 imports statements and bundles only the code which is used. Pretty cool … but … it is a JavaScript bundler only and there are no plugins for Gulp, my favourite task runner. In this article I will show you, how to integrate Rollup in your Gulp bundling pipeline. Install RollupBest practice is to install Rollup globally: 1npm install --global rollup The Gulp FileStarting point was my gulpfile.js as follows: gulpfile.js1234567891011121314151617181920212223242526const { src, dest, watch, series, parallel } = require('gulp');const del = require('del');const cssimport = require("gulp-cssimport");const cleancss = require('gulp-clean-css');const sourcemaps = require('gulp-sourcemaps');/* Clean distribution folder */function clean() { return del('./dist/**', { force: true });}/* Bundle CSS with sourcemapping, imports and cleaning */function css() { return src('./styles/app.css') .pipe(sourcemaps.init()) .pipe(cssimport({})) .pipe(cleancss({ debug: true }, (details) => { console.log(`${details.name} BEFORE: ${details.stats.originalSize}`); console.log(`${details.name} AFTER: ${details.stats.minifiedSize}`); })) .pipe(sourcemaps.write('.', { sourceRoot: '/styles' })) .pipe(dest('./dist/'));}exports.default = series(clean, css); This pipeline only bundles CSS yet, when calling gulp in the command line. Calling Rollup for JS bundlingRollup has dozens of parameters to define everything you need, but it also supports a config file, which allows you to configure everything there and run rollup -c only. Very useful on this approach. rollup.config.js12345678export default { input: './js/app.js', output: { file: './dist/app.js', format: 'es', sourcemap: true }}; As there is no Gulp plugin for Rollup, we need to execute Rollup in the Gulp pipeline by command. For this I’ve created a helper in my gulpfile.js, to be able to execute whichever command: gulpfile.js1234567891011let HELPERS = { execute: (command) => { const process = exec(command); process.stdout.on('data', (data) => { console.log(data.toString()); }) process.stderr.on('data', (data) => { console.log(data.toString()); }) process.on('exit', (code) => { console.log('Process exited with code ' + code.toString()); }) return process; }} This helper is used in a Gulp command function to call Rollup: gulpfile.js123function javascript() { return HELPERS.execute('rollup -c');} The last thing I had to do, is to insert the command in the pipeline to run in parallel to the CSS bundling: gulpfile.js1exports.default = series(clean, parallel(css, javascript)); Pretty straightforward, isn’t it? Happy bundling with Rollup and Gulp…","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"SPA","slug":"SPA","permalink":"https://kiko.io/tags/SPA/"},{"name":"Bundling","slug":"Bundling","permalink":"https://kiko.io/tags/Bundling/"}]},{"title":"Automatic Duplicate Image Shadow","subtitle":"Drop an image shadow with the image itself via JavaScript","date":"2021-07-16","updated":"2021-07-16","path":"post/Automatic-Duplicate-Image-Shadow/","permalink":"https://kiko.io/post/Automatic-Duplicate-Image-Shadow/","excerpt":"At the beginning of the year I wrote a post about showing a shadow on an image with the image itself instead of using box-shadow, to make the image appear glass-like. Nice trick, but it would be much easier to have a little script, that does this automatically for all images on a page. In this post I will show you how to achieve this.","keywords":"beginning year wrote post showing shadow image box-shadow make glass-like nice trick easier script automatically images page show achieve","text":"At the beginning of the year I wrote a post about showing a shadow on an image with the image itself instead of using box-shadow, to make the image appear glass-like. Nice trick, but it would be much easier to have a little script, that does this automatically for all images on a page. In this post I will show you how to achieve this. The script is not really rocket science: We just have to surround an image tag…. 1<img src="my-image.jpg" /> … with a wrapper, which holds the original image and a blurred duplicate of it: 1234<div class="shadow-wrapper"> <img class="drop-shadow" src="my-image.jpg" /> <img class="shadow" src="my-image.jpg" /></div> The rest will be done by CSS: 123456789101112131415161718192021222324div.shadow-wrapper { /* Wrapper */ position: relative; margin-bottom: 30px;}div.shadow-wrapper img.drop-shadow { /* Original image */ position: absolute; left: 0; top: 0; width: 100%; z-index: 1; margin: 0; float: none;}div.shadow-wrapper img.shadow { /* Shadow image */ position: absolute; width: 90%; left: 5%; top: 15%; z-index: 0; filter: blur(10px); opacity: 0.8; margin: 0; float: none;} To make long story short: The wrapper is positioned relative and both images absolute. The shadow image is 10% smaller than the original, blurred by 10 pixels and lies beneath the original one, slightly shifted down by 15%. Very important is to give the wrapper a bottom margin, otherwise the shadow will be rendered to tight to other elements, what looks not good. The ScriptWhat the script should do: Take any image tag with a particular class name (here drop-shadow) Create a new wrapper with all classes the image tag has Size the wrapper as the image Place the image tag inside the wrapper Clone the image tag and append it to the wrapper also Replace the image tag with the new wrapper HTML 1234567891011121314151617181920document.querySelectorAll("img.drop-shadow").forEach(function(item) { let wrapper = document.createElement("div"); wrapper.classList.add("shadow-wrapper"); item.classList.forEach(function(c) { if (c != "drop-shadow") wrapper.classList.add(c); }); wrapper.style.width = item.clientWidth + "px"; wrapper.style.height = item.clientHeight + "px"; wrapper.insertAdjacentHTML("beforeend", item.outerHTML); let shadow = item.cloneNode(); shadow.classList.remove("drop-shadow"); shadow.classList.add("shadow"); wrapper.insertAdjacentHTML("beforeend", shadow.outerHTML); item.outerHTML = wrapper.outerHTML;} The result in comparison with no shadow and the default box-shadow: Happy shadowing…","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"},{"name":"DOM","slug":"DOM","permalink":"https://kiko.io/tags/DOM/"}]},{"title":"Generate Social Media Images Automatically","subtitle":null,"date":"2021-07-10","updated":"2021-07-10","path":"post/Generate-Social-Media-Images-Automatically/","permalink":"https://kiko.io/post/Generate-Social-Media-Images-Automatically/","excerpt":"From day one of this blog I wanted to combine two of my passions: tech stuff and photography. All these photos I have shot myself in recent years and now they are representing my thoughts & findings about digital technology. I wrote about my approach to provide these images in my post Automatic Header Images in Hexo. When I share one of my posts on social media I provide the appropriate image as a visual anchor to my writing. The technique behind this are the meta tags in the HTML of my posts: 123456789<!-- Schema.org for Google --><meta itemprop="image" content="https://kiko.io/photos/normal/DSC_6776.jpg"><!-- Open Graph --><meta property="og:image" content="https://kiko.io/photos/normal/DSC_6776.jpg"><!-- Twitter --><meta property="twitter:card" content="summary_large_image"><meta property="twitter:image" content="https://kiko.io/photos/normal/DSC_6776.jpg"> There are several meta tags for different purposes regarding images. For more information see the links at the end of this post. To make a long story short: The sum of these approaches ensures that when an article is posted, the corresponding image is also displayed in the social media post. But … it’s only the image, without a visual reference to the post itself. In this article I want to show you how to combine the photo with some meta information of the post automatically, to get a Social Media Image.","keywords":"day blog wanted combine passions tech stuff photography photos shot recent years representing thoughts & findings digital technology wrote approach provide images post automatic header hexo share posts social media image visual anchor writing technique meta tags html 123456789<-- schemaorg google --><meta itemprop="image" content="https://kiko.io/photos/normal/DSC_6776.jpg"><!-- open graph property="ogimage" twitter property="twittercard" content="summary_large_image"><meta property="twitterimage" content="https://kiko.io/photos/normal/DSC_6776.jpg"> purposes information links end make long story short sum approaches ensures article posted displayed … reference show photo automatically","text":"From day one of this blog I wanted to combine two of my passions: tech stuff and photography. All these photos I have shot myself in recent years and now they are representing my thoughts & findings about digital technology. I wrote about my approach to provide these images in my post Automatic Header Images in Hexo. When I share one of my posts on social media I provide the appropriate image as a visual anchor to my writing. The technique behind this are the meta tags in the HTML of my posts: 123456789<!-- Schema.org for Google --><meta itemprop="image" content="https://kiko.io/photos/normal/DSC_6776.jpg"><!-- Open Graph --><meta property="og:image" content="https://kiko.io/photos/normal/DSC_6776.jpg"><!-- Twitter --><meta property="twitter:card" content="summary_large_image"><meta property="twitter:image" content="https://kiko.io/photos/normal/DSC_6776.jpg"> There are several meta tags for different purposes regarding images. For more information see the links at the end of this post. To make a long story short: The sum of these approaches ensures that when an article is posted, the corresponding image is also displayed in the social media post. But … it’s only the image, without a visual reference to the post itself. In this article I want to show you how to combine the photo with some meta information of the post automatically, to get a Social Media Image. Starting point of my thoughts were two posts from Drew McLellan (Dynamic Social Sharing Images) and Ryan Filler (Automatic Social Share Images), to which I have already referred in my post Discoveries #11. Drew and Ryan utilizes the Node.JS library Puppeteer, which runs a headless Chromium (or Chrome browser) over the DevTools protocol to process a web page … for example to take a screenshot: 12345678const puppeteer = require('puppeteer');(async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com'); await page.screenshot({ path: 'example.png' }); await browser.close();})(); The idea is, to create a temporary HTML page with the photo and all necessary text for the social media image, take a screenshot of it and save it as PNG. As I run my blog with Hexo, a Static Site Generator (SSG), all information about a post is defined in a Markdown (MD) file with some Frontmatter for the meta information. Therefore, the Social Media Image Generator script in my mind had to do following tasks: Iterate recursively over all MD files in Hexo _source/posts folder Read the MD’s Frontmatter (for information about photo, title, subtitle and more) Create a temporary HTML file with the aid of a template Run Puppeteer script over the temporary file to take a screenshot Store the PNG to a central folder Optimize the PNG Change the meta tags in the posts to reference the new image The FrontmatterI pimped the Frontmatter of the original Hexo configuration a bit, in order to provide an individual photo for each post: 123456789101112---title: Generate Social Media Images Automaticallysubtitle:date: 2021-07-10 11:07:31photograph: file: DSC_6776.jpg name: Color Brushes link: 'https://500px.com/photo/79965349'categories: - JavaScript...--- Among other, there are the basic information, I wanted to have on my social media image: photograph.file (as the image itself) and title, subtitle and categories (for the text on the image). The ScriptThe complete script, in two versions (CommonJS and ES Module) is available at GitHub. tl;dr My script became a JavaScript class, separating the tasks in several methods and a constructor to get all necessary information as parameters. The class exports the main method generate() for calling the script: social-media-image-generator.cjs12345678910111213141516171819202122232425const _currentPath = __dirname;var _postFolder;var _photoFolder;var _templateFile;var _targetFolder;class Generator { constructor(postFolder, photoFolder, templateFile, targetFolder) { _postFolder = path.join(_currentPath, postFolder); _photoFolder = path.join(_currentPath, photoFolder); _templateFile = path.join(_currentPath, templateFile); _targetFolder = path.join(_currentPath, targetFolder); } generate() { ... } getPostFiles(dirPath, allFiles) { ... } async processPost(fileName, vars) { ... } async createImage(fileName, tempFile) { ... }}module.exports.Generator = Generator I chose parameters, in order not to bind the script too tightly to my favourite SSG Hexo: _postFolder - Where are the post files stored? _photoFolder - Where are the photos stored? _templateFile - Where is the template file for the temporary HTML stored? _targetFolder - Where should the generated PNG files be stored? Get the postsFirst task was to get all MD files out of the _postFolder recursively: social-media-image-generator.cjs123456789101112131415161718192021222324252627282930const fs = require("fs");const path = require("path");class Generator { generate() { const postFiles = this.getPostFiles(_postFolder); } getPostFiles(dirPath, allFiles) { // READ FOLDER CONTENT let files = fs.readdirSync(dirPath); //INIT TEMP ARRAY allFiles = allFiles || []; files.forEach((file) => { if (fs.statSync(dirPath + "/" + file).isDirectory()) { // CALL THE METHOD RECURSIVELY allFiles = this.getPostFiles(dirPath + "/" + file, allFiles) } else if (file.indexOf(".md")>=0) { // PUSH MD FILES TO TEMP ARRAY allFiles.push(path.join(dirPath, "/", file)) } }); return allFiles; } }} Get the template and the temporary folderI chose Handlebars as the template engine to generate the temporary HTML file, because it is so easy to handle. social-media-image.handlebars1234567891011121314151617181920212223242526<html> <head> <style> ... </style> </head> <body> <div class="wrap"> ... <img id="photo" src="{{photo}}"> <div class="container"> ... <section id="title"> {{#each categories}} <small>{{this}}</small> {{/each}} <h1>{{title}}</h1> {{#if subtitle}} <h2>{{subtitle}}</h2> {{/if}} </section> </div> </div> </body></html>... Handlebars is able to compile a template into a JavaScript variable, what makes it easy to reuse it. Good for performance and stability. As I wanted to utilize the template to generate temporary HTML files, I needed a temporary folder, which can be deleted afterwards. social-media-image-generator.cjs123456789101112131415161718192021222324const handlebars = require("handlebars");var _template;const _tempFolder = "./~temp";class Generator { constructor(postFolder, photoFolder, templateFile, targetFolder) { ... // GET THE TEMPLATE CONTENT let source = fs.readFileSync(_templateFile).toString('utf8'); // COMPILE THE TEMPLATE FOR FURTHER USE ONCE _template = handlebars.compile(source); // CREATE TEMP FOLDER IN THE WORKING DIRECTORY if (!fs.existsSync(_tempFolder)) { fs.mkdirSync(_tempFolder); } }} Process the postsSecond step was to process all the posts found. social-media-image-generator.cjs1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374const fs = require("fs");const path = require("path");const url = require("url");const frontmatter = require("front-matter");const _tempFolder = "./~temp";class Generator { generate() { let self = this; const postFiles = this.getPostFiles(_postFolder); var postsProcessed = 0; // ITERATE OVER ALL POSTS postFiles.forEach((file) => { fs.readFile(file, 'utf8', function(err, data) { if (err) throw err // READ THE FRONTMATTER let content = frontmatter(data); let fileName = path.basename(file, path.extname(file)); // only process posts with defined photograph file // and if social media file is missing if (content.attributes.photograph?.file && !fs.existsSync(path.join(_targetFolder, fileName + ".png"))) { // CALL PROCESSING METHOD self.processPost( fileName, { title: content.attributes.title, subtitle: content.attributes.subtitle, categories: content.attributes.categories, photo: url.pathToFileURL( path.join(_photoFolder, content.attributes.photograph.file) ) }) .then(() => { // DELETE TEMP FOLDER AFTER PROCESSING if (postsProcessed === postFiles.length) { fs.rmdirSync(_tempFolder, { recursive: true }); } }); } postsProcessed += 1; }) }); } async processPost(fileName, vars) { // GET HTML FOR POST VIA HANDLEBARS let html = _template(vars); let tempFile = path.join(_tempFolder, fileName + ".html"); //WRITE TEMPORARY HTML FILE fs.writeFile(tempFile, html, (err) => { if(err) { throw(err); } //console.log(tempFile + " saved"); }); //CALL IMAGING METHOD await this.createImage(fileName, tempFile); return; }} Get the imageAs I had the temporary HTML file now, I only had to open up a Puppeteer instance, load the file and take the screenshot: 12345678910111213141516171819202122232425262728293031323334353637383940414243const puppeteer = require("puppeteer/cjs-entry");const imagemin = require("imagemin");const imageminPngquant = require("imagemin-pngquant");class Generator { async createImage(fileName, tempFile) { var self = this; // LAUNCH CHROMIUM AND A NEW PAGE const browser = await puppeteer.launch(); const page = await browser.newPage(); // LOAD THE TEMPORARY HTML FILE await page.goto(url.pathToFileURL(tempFile)); // SET THE EXACT WIDTH & HEIGHT await page.setViewport({ width: 1200, height: 630, deviceScaleFactor: 1 }); let imgFile = path.join(_targetFolder, fileName + ".png"); // TAKE SCREENSHOT INTO PNG FILE AT TARGET FOLDER await page.screenshot({ path: imgFile }); await browser.close(); // OPTIMIZE THE PNG FILE await imagemin([imgFile], 'build', { plugins: [ imageminPngquant({ quality: '75-90' }) ] }); return; }} Running the scriptIf you already have lots of post in MD files and appropriate photographs, you can create an execution script… run-social-media-images.cjs123456789const Generator = require("./social-media-image-generator.cjs").Generator;const postFolder = process.argv[2].toString();const photoFolder = process.argv[3].toString();const templateFile = process.argv[4].toString();const targetFolder = process.argv[5].toString();const generator = new Generator(postFolder, photoFolder, templateFile, targetFolder);generator.generate(); … and run it as follows: Example execution in the console...1node "./lib/run-social-media-images.cjs" "../source/_posts" "../static/photos/normal" "../templates/social-media-image.handlebars" "../static/images/social-media" Hexo IntegrationIn case you are running your blog with Hexo also, you can hook on the ready event to let it run on hexo generate automatically: /scripts/on-ready-generate-social-media-images.js1234567891011121314151617181920const log = require('hexo-log')({ debug: false, silent: false});const Generator = require("../lib/social-media-image-generator.cjs").Generator;hexo.on("ready", function() { log.info("Running Social-Media-Image-Generator..."); const postFolder = "../source/_posts"; const photoFolder = "../static/photos/normal"; const templateFile = "../templates/social-media-image.handlebars"; const targetFolder = "../static/images/social-media"; const generator = new Generator(postFolder, photoFolder, templateFile, targetFolder); generator.generate();}); It is important not to store the social-media-image-generator.cjs in Hexo’s scripts folder like the event script above, because Hexo will try to execute it automatically. You have to create a different folder like lib to store and reference it from there. The ResultHere is the result from my approach in Hexo, as I run hexo generate for this blog post: The very last thing I had to do, was to change the source of the image meta tag mentioned at the top, to reference to newly created social media image. Here’s the new image in action at Twitter: More Info CSS Tricks: The Essential Meta Tags for Social MediaThe Open Graph Protocol: The Open Graph ProtocolDrew McLellan: Dynamic Social Sharing ImagesRyan Filler: Automatic Social Share ImagesThe GitHub Blog: A framework for building Open Graph images","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"},{"name":"ES6","slug":"ES6","permalink":"https://kiko.io/tags/ES6/"}]},{"title":"Discoveries #11","subtitle":null,"series":"Discoveries","date":"2021-06-28","updated":"2021-06-28","path":"post/Discoveries-11/","permalink":"https://kiko.io/post/Discoveries-11/","excerpt":"Todays issue of Discoveries is all about images. Show them, manipulate them, get information about them, protect them, use them and create them with JavaScript. Have fun trying out one of these cool solutions. PhotoSwipelightGalleryPicaResemble.jsmedium-zoomLC-Mouse-DragProtectImage.jsTinygraphsDynamic Social Sharing ImagesAutomatic Social Share Images","keywords":"todays issue discoveries images show manipulate information protect create javascript fun cool solutions photoswipelightgallerypicaresemblejsmedium-zoomlc-mouse-dragprotectimagejstinygraphsdynamic social sharing imagesautomatic share","text":"Todays issue of Discoveries is all about images. Show them, manipulate them, get information about them, protect them, use them and create them with JavaScript. Have fun trying out one of these cool solutions. PhotoSwipelightGalleryPicaResemble.jsmedium-zoomLC-Mouse-DragProtectImage.jsTinygraphsDynamic Social Sharing ImagesAutomatic Social Share Images PhotoSwipe by Dmytro Semenov https://photoswipe.com PhotoSwipe is a simple, but powerful image gallery written with jQuery, which has plugin support for some CRM’s. Currently Dmytro is working on the next version v5. lightGallery by Sachin Neravath https://www.lightgalleryjs.com/ Another wionderful image gallery, but with more (useful?) images features, realized as plugins. It supports video and IFrames also and is fully responsive. Pica by Nodeca https://github.com/nodeca/pica Pica is an Open Source project for resizing images with JavaScript. It supports several technologies, like WebAssembly, WebWorkers or even pure JS. Resemble.js by James Cryer http://rsmbl.github.io/Resemble.js/ Analyse and compare images with Javascript and visualizes the differences. Great stuff. medium-zoom by François Chalifour https://github.com/francoischalifour/medium-zoom Zoom your images on your website to fullsize like on medium.com. LC-Mouse-Drag by Luca from LCWeb Italia https://github.com/LCweb-ita/LC-Mouse-Drag Instead of zooming your image on your website, you can let the users scroll into an oversized image, to see the details. ProtectImage.js by ColonelParrot https://github.com/ColonelParrot/ProtectImage.js ProtectImage.js is a Javascript library that helps prevent image theft by disabling traditional user interactions to download/copy images. Tinygraphs by Taironas https://www.tinygraphs.com/ Supporting avatars in the comment section of a website is cool, but not every user wants to share an image. Tinygraphs helps out with random graphic images. Dynamic Social Sharing Images by Drew McLellan https://24ways.org/2018/dynamic-social-sharing-images/ Sharing posts on Social Media is vital to get readers and it is a good idea to provide a generic post image. Drew shows how to utilize Node.js to get a page screenshot automatically. The posted script has some pitfalls, but I will post a HowTo shortly. Automatic Social Share Images by Ryan Filler https://www.ryanfiller.com/blog/automatic-social-share-images/ Having a screenshot of a page is one side of the coin on creating Social Share images. Ryan shows us, how to enrich them with the appropriate text.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Open Source Insights - Seeing the big picture","subtitle":"Google's new experimental platform to get a grip on project dependencies","series":"Great Finds","date":"2021-06-06","updated":"2021-06-06","path":"post/Open-Source-Insights-Seeing-the-big-picture/","permalink":"https://kiko.io/post/Open-Source-Insights-Seeing-the-big-picture/","excerpt":"A while ago I needed some functionality regarding database access in one of my spare time projects and I decided to use a library from NPM. Typed npm install and the hell was breaking loose … 186 direct dependencies and nearly 200K of files were flooding my harddrive! The mental basis of IT is lazyness, which means that we produce software to make our and others life easier. This also applies to the building process. Don’t reinvent the wheel, but reuse the work of other developers. But … we have to recognize the limits and prevent to fall into the dependency hell. To get a better overview over dependencies, regarding NPM and other repositories, some Google engineers have published a project called Open Source Insights a couple of days ago.","keywords":"ago needed functionality database access spare time projects decided library npm typed install hell breaking loose … direct dependencies 200k files flooding harddrive mental basis lazyness means produce software make life easier applies building process dont reinvent wheel reuse work developers recognize limits prevent fall dependency overview repositories google engineers published project called open source insights couple days","text":"A while ago I needed some functionality regarding database access in one of my spare time projects and I decided to use a library from NPM. Typed npm install and the hell was breaking loose … 186 direct dependencies and nearly 200K of files were flooding my harddrive! The mental basis of IT is lazyness, which means that we produce software to make our and others life easier. This also applies to the building process. Don’t reinvent the wheel, but reuse the work of other developers. But … we have to recognize the limits and prevent to fall into the dependency hell. To get a better overview over dependencies, regarding NPM and other repositories, some Google engineers have published a project called Open Source Insights a couple of days ago. If you look at a library on NPM, you immediately see the direct dependencies, but that could just be the tip of the iceberg. Insights shows you the big picture in form of an interactive graph or a complete list with many additional information of the complete dependency chain. As Google says: Your software and your users rely not only on the code you write, but also on the code your code depends on, the code that code depends on, and so on. An accurate view of the complete dependency graph is critical to understanding the state of your project. Next time, before you type npm install ... take a quick look at the Insights page of the library, because it won’t give you an overview over the dependecies only, but also some security advisories to let you decide if it is a good idea to use it. Very valueable for the authors of exisiting NPM projects also, because it shows the consequences of dependency decisions very clearly and gives hints to make the own library less sensitive to vulnerabilities. Let us hope, that Google removes the describing adjective experimental from the project some day and adds more and more functionality to it (like a working responsiveness ;) in order to establish Open Source Insights as a main source of information about reusable Open Source software.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Node.js","slug":"Node-js","permalink":"https://kiko.io/tags/Node-js/"}]},{"title":"Discoveries #10","subtitle":null,"series":"Discoveries","date":"2021-05-24","updated":"2021-05-24","path":"post/Discoveries-10/","permalink":"https://kiko.io/post/Discoveries-10/","excerpt":"Todays Discoveries it’s all about my favourite programming language JavaScript. Some tiny tips and tricks alongside with a deep dive into ‘console’ and some helful UI libraries. Have fun… Beyond Console.log()DOMGuard - Stop scammers from the manipulating DOMHandling User Permissions in JavaScripthtml-chain - Make html by chaining javascript functionsAccessible AutocompleteJS DataTableMK ChartsSnabbt.js - Fast animations with Javascript and CSS transformsSimplyLazy - Pure JavaScript Image Lazy LoaderBlury-Loading","keywords":"todays discoveries favourite programming language javascript tiny tips tricks alongside deep dive console helful ui libraries fun… consolelogdomguard - stop scammers manipulating domhandling user permissions javascripthtml-chain make html chaining functionsaccessible autocompletejs datatablemk chartssnabbtjs fast animations css transformssimplylazy pure image lazy loaderblury-loading","text":"Todays Discoveries it’s all about my favourite programming language JavaScript. Some tiny tips and tricks alongside with a deep dive into ‘console’ and some helful UI libraries. Have fun… Beyond Console.log()DOMGuard - Stop scammers from the manipulating DOMHandling User Permissions in JavaScripthtml-chain - Make html by chaining javascript functionsAccessible AutocompleteJS DataTableMK ChartsSnabbt.js - Fast animations with Javascript and CSS transformsSimplyLazy - Pure JavaScript Image Lazy LoaderBlury-Loading Beyond Console.log() by Christian Heilmann https://www.sitepoint.com/beyond-console-log-level-up-your-debugging-skills/ The browser console is propably the most used tool for debugging JavaScript, but most of the time we all just scatch the surface. Christian show us the power of the console. DOMGuard - Stop scammers from the manipulating DOM by David Wells https://dom-guard.netlify.app/ There are many attack vectors scammers use to draw money out of the pockets. You have to make it as difficult as possible for them. Davids idea is to protect the DOM of the browser against changes utilizing the JS MutationObserver. Clever. Handling User Permissions in JavaScript by Andreas Remdt https://css-tricks.com/handling-user-permissions-in-javascript/ In case you have to intregrate a permission system into your Web App, to separate features from different user groups, Andreas post on CSS Tricks is a very good entry point into the subject. html-chain - Make html by chaining javascript functions by Matthew Elphick https://github.com/maael/html-chain There are several ways on dealing with HTML in JavaScript. My favourite approach are literals. Matthew gives us with his library the possibility to do it in a LINQ-style by chaining commands. Accessible Autocomplete by Government Digital Service https://github.com/alphagov/accessible-autocomplete Many cool looking UI elements on the web are not accessible for the impaired. But especially public services has to be aware of that. Developers from the British Government Digital Service have created a full WAI-ARIA compatible library for autocomplete inputs. JS DataTable by Luigi Verolla https://github.com/luverolla/js-datatable Deaing with tables in HTML can be a mess, when you try to add some functionality like searching, sorting and paging and that also responsive. Take a nap, because Luigi has a fully functional solution for this. MK Charts by Marcus Kirschen https://mkirschen.de/mk-scripts/mk-charts/ Dashboards everywhere. In case you don’t have a specialized UI library and just want to add some circle charts to your UI, try out Marcus’ solution. Just define the values in your HTML tag and let MK Charts do the rest. Simple and easy. Snabbt.js - Fast animations with Javascript and CSS transforms by Daniel Lundin https://daniel-lundin.github.io/snabbt.js/ Snabbt is quite old in terms of the IT industry, but still worth mentioning, because it is a really light and fast solution for adding animations to your Web App. See the demos … it’s still stunning. SimplyLazy - Pure JavaScript Image Lazy Loader by Max (maxshuty) https://maxshuty.github.io/simply-lazy/ Lazy loading can be a must on image heavy webs and you got bazillion results while searching for the right JS library. I can recommend Max’s solution, because it’s quite tiny and has callback as well as default image support. Blury-Loading by S.M.Abtahi Noor https://github.com/19smabtahinoor/Blury-Loading Apropos loading … maybe you want to preload your Web App’s sources completely and show the user a loading visual? Take this nice looking approach: while a percentage figure is running upwards, the background image is getting less blurry. A three-liner, but cool. Thanks Mr. Noor.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Hexo and the IndieWeb (Receiving Webmentions)","subtitle":"Use webmention.io the easy way","series":"IndieWeb","date":"2021-05-13","updated":"2021-05-13","path":"post/Hexo-and-the-IndieWeb-Receiving-Webmentions/","permalink":"https://kiko.io/post/Hexo-and-the-IndieWeb-Receiving-Webmentions/","excerpt":"This is part three of the splitted original post Hexo and the IndieWeb. Don’t miss Part 2 Hexo and the IndieWeb (Sending Webmentions) either. A meaningful interaction has always two directions: sending and receiving. In this part of the post I want to show you how to receive Webmentions from other blogs participating in the IndieWeb. As Hexo is a SSG it generates static HTML pages. This has the advantage that the pages can be hosted just about anywhere (in my case Github Pages, but also the disadvantage of not having a real backend. Therefore, we need an external service that acts as an Webmention endpoint, where other people can send their webmentions. Aaron Parecki, co-founder of the IndieWeb, has made a service called webmention.io we can use for free. It is able to convert old-fashioned Pingbacks to Webmentions, supports deleting of unwanted mentions, has a Blocklist for blocking domains, Webhooks for real-time processing and last but not least an API to get all your Webmentions per page or per site.","keywords":"part splitted original post hexo indieweb dont miss sending webmentions meaningful interaction directions receiving show receive blogs participating ssg generates static html pages advantage hosted case github disadvantage real backend external service acts webmention endpoint people send aaron parecki co-founder made called webmentionio free convert old-fashioned pingbacks supports deleting unwanted mentions blocklist blocking domains webhooks real-time processing api page site","text":"This is part three of the splitted original post Hexo and the IndieWeb. Don’t miss Part 2 Hexo and the IndieWeb (Sending Webmentions) either. A meaningful interaction has always two directions: sending and receiving. In this part of the post I want to show you how to receive Webmentions from other blogs participating in the IndieWeb. As Hexo is a SSG it generates static HTML pages. This has the advantage that the pages can be hosted just about anywhere (in my case Github Pages, but also the disadvantage of not having a real backend. Therefore, we need an external service that acts as an Webmention endpoint, where other people can send their webmentions. Aaron Parecki, co-founder of the IndieWeb, has made a service called webmention.io we can use for free. It is able to convert old-fashioned Pingbacks to Webmentions, supports deleting of unwanted mentions, has a Blocklist for blocking domains, Webhooks for real-time processing and last but not least an API to get all your Webmentions per page or per site. Setupwebmention.io needs a registration and Aaron uses his own authentication method Web Sign-In over IndieLogin to achieve that. After you have signed in, you see your settings with two important links, you have to integrate in the head of your HTML: 12<link rel="webmention" href="https://webmention.io/[YOUR-BLOG-DOMAIN]/webmention" /><link rel="pingback" href="https://webmention.io/[YOUR-BLOG-DOMAIN]/xmlrpc" /> They define your page on the one hand as a webmention- and on the other hand as a pingback endpoint. Every send Webmention or Pingback from another blog is routed to these URL’s. You will also find your API Key on this page. To be honest, is not a real API key, but rather a key to retrieve all incoming webmentions at once. Therefore it does not need to be kept private and secured. There is no way to modify your incoming Webmentions over it. Incoming Webmentions I’m implementing my Hexo/Webmention solution as I’m writing this post, therefore I don’t have any Webmentions to show as an example. Since all mentions on webmention.io are publicly accessible, I simply use Max Böcks article Using Webmentions in Eleventy in the following. Max, I hope it is ok … :) Aaron has defined three ways to get the incoming mentions of a particular article on your blog: 1. View as HTML page https://webmention.io/api/mentions.html?target=https://mxb.dev/blog/using-webmentions-on-static-sites/ Using it this way, webmention.io integrates the content with some styles and other information of the sending post for easy reading. 2. Consume as Atom feed https://webmention.io/api/mentions.atom?target=https://mxb.dev/blog/using-webmentions-on-static-sites/ 3. Get as JSON https://webmention.io/api/mentions.jf2?target=https://mxb.dev/blog/using-webmentions-on-static-sites/ Especially the last one is interesting for us, because we can use it to automate getting the data and integrate it in our blog post. You can also use all these Url’s to get the Webmentions of all of your pages, by changing the target parameter into: ...?token=[YOUR-API-KEY] The feed with token for getting all webmentions is particularly practical for checking whether there are new ones for your blog via a feed reader. There are some parameters you have to be aware of: Parameter Example Paging ?per-page=20&page=0 (default, page 1 with 20 entries) Sorting By ?sort-by=created (default) Sorting Direction ?sort-dir=down (default, newest first) Time Limit ?since=2021-05-10T12:00:00-0700 ID Limit ?since_id=500). You can find more on parameters in the documentation of webmention.io’s source code here: https://github.com/aaronpk/webmention.io. The DataLet’s dive into the data. The JSON is a list and every Webmention is an entry underneath children, with following useful fields: Field Purpose author.name Name of the sender author.photo Avatar photo of the sender author.url Personal URL of the sender wm-id Unique ID of the Webmention wm-target URL of your post wm-received UTC Date/Time sended wm-source URL of the sending post published Publish Date/Time of the sending post wm-property Webmention type out of the following values:mention-of : Mention as link in postin-reply-to : Reply to your postlike-of : Like of your postrepost-of : Repost of your postbookmark-of : Bookmark of your post Other occuring fields are optional, but no less interesting for displaying them at your post: Field Purpose name Name of the Webmention (title of the sending post or something) summary.type Summary type of the Webmention, e.g. “text/plain” summary.value Summary text of the Webmention content.html Content as HTML of sending post content.text Content as text of sending post The fact that we get the complete HTML content of the sending post, means that we easily can parse it for IndieWeb microformats (see Part 1) to get even more information about it and the blog owner! IntegrationWe have two ways to choose from in order to integrate the data thus obtained into the post: Static … means while generating the site Dynamic … means real-time via client-side Javascript The static approach has two big disadvantages: The Webmentions are only visible with a time delay, due to the need of generating the pages of the site Hexos generation mechanism only takes those, whose content has beed updated in the meanwhile, but do not take into account any external data like Webmentions. We would have to fire up the time consuming clean and generate all the time So, let’s go for the dynamic approach, a little bit JavaScript, which loads the Webmentions on page load (like most Commenting platforms do). First of all, we need a placeholder in our article.ejs, where the Webmentions will be displayed. Somewhere near the comments and implemented as another partial: layout/_partial/article.ejs12345678910111213<article id="<%= post.layout %>-<%= post.slug %>" class="article article-type-<%= post.layout %> h-entry" itemscope itemprop="blogPost"> ... <%- partial('post/webmentions') %> <%- partial('post/comments') %> <%- partial('post/related') %> <%- partial('post/nav') %></article> layout/_partial/post/webmentions.ejs12345678910111213<div class="article-webmentions"> <div class="webmentions-placeholder"> <p class="wm-placeholder">No Webmentions yet...</p> </div> <script> window.addEventListener('load', function () { insertWebmentions('<%- post.slug.toLowerCase() %>'); }) </script></div> The call insertWebmentions references to the asset script webmentions.js, which will be bundled via Grunt in my case, but you can place it where you want and add it to your articles head element. The main thing is, that it will load when the article page is launched. The script checks, if the user has already loaded the Webmentions for the current page from webmention.io in the last hour and do so if not, and stores the data in the browser by the pages key (slug) and with a timestamp: assets/webmentions.js12345678910111213141516171819202122232425262728293031323334353637383940414243function insertWebmentions(key) { const lsTimestamp = "wmts_" + key; const lsWebmentions = "wm_" + key; const currentUrl = window.location.href; const wmUrl = `https://webmention.io/api/mentions.jf2?target=${currentUrl}&per-page=1000&sort-dir=up`; let lastRequest; let webmentions; // Get data from browser storage, if available if (localStorage.getItem(lsTimestamp) && localStorage.getItem(lsWebmentions)) { lastRequest = localStorage.getItem(lsTimestamp); webmentions = JSON.parse(localStorage.getItem(lsWebmentions)); } if(webmentions && lastRequest && Math.abs(Date.now() - lastRequest) / (60*60*1000) < 1) { // Webmentions are present and not older than an hour process(); } else { // Get Webmentions from webmention.io load().then(() => { process(); }); }; /** * Load webmention.io's JSON data for the current page */ async function load() { const response = await fetch(wmUrl); webmentions = await response.json(); localStorage.setItem(lsWebmentions, JSON.stringify(webmentions)); localStorage.setItem(lsTimestamp, Date.now()); } /** * Process Webmentions */ function process() { ... }} Actually the script loads the first 1000 Webmentions in ascending order of their retreiving date. Should last for a while or I may feel like adding real paging at some point ;) On processing the webmentions, a separate HTML block is generated for each type (wm-property) regarding the content of the Webmention, whereby the header is always the same. The VERB refers to the Webmentions type: mentioned, replied, liked, bookmarked or reposted. HEADER1234567891011121314<div class="wm-card h_card"> <a class="wm-photo-link u-url" href="[AUTHOR.URL]"> <img class="wm-photo u-photo" width="44" height="44" src="[AUTHOR.PHOTO]" alt="[AUTHOR.NAME]"> </a> <div class="wm-meta"> <a class="wm-name p-name" href="[AUTHOR.URL]">[AUTHOR.NAME]</a> <span class="wm-verb">[VERB] on</span> <time class="wm-date dt-published" datetime="[WM-RECEIVED]"> [formatted WM-RECEIVED] </time> <small>[Running Number]</small> </div></div> The HTML of the five implemented types: MENTION1234567<div class="webmention wm-mentioned" id="[WM-ID]"> [HEADER HTML] <div class="wm-content p-content"> <p>[First 50 words of CONTENT.TEXT with ellipsis]</p> <a class="wm-source" href="[WM-SOURCE]">[WM-SOURCE]</a> </div></div> REPLY1234567<div class="webmention wm-mentioned" id="[WM-ID]"> [HEADER HTML] <div class="wm-content p-content"> <p>[Complete CONTENT.HTML]</p> <a class="wm-source" href="[WM-SOURCE]">[WM-SOURCE]</a> </div></div> LIKE & BOOKMARK (no content)123<div class="webmention wm-mentioned" id="[WM-ID]"> [HEADER HTML]</div> REPOST12345678<div class="webmention wm-mentioned" id="[WM-ID]"> [HEADER HTML] <div class="wm-content p-content"> <p> ... at <a href="[WM-SOURCE]">[WM-SOURCE]</a> </p> </div></div> The result will look like this, after adding some styles: You can download the complete JavaScript- and the Stylus file on Github:https://github.com/kristofzerbe/Hexo-and-the-IndieWeb-Files Form for sending Webmentions manuallyAs you may noticed in Part 2, some manual work is necessary to have webmentions sent. Therefore, it is good to give the user a possibility to submit his blog post, where he mentions yours, directly. As webmentions.io supports posting a new source, the HTML form is quite simple an we can integrate it in the partial from above, below the script tag: layout/_partial/post/webmentions.ejs1234567891011121314151617181920<div class="article-webmentions"> ... <form class="webmention-form" action="https://webmention.io/<%- config.title %>>/webmention" name="webmention-form" method="post"> <label for="webmention-form-source">Your Article URL:</label><br> <input class="webmention-form-source" type="url" name="source" placeholder="https://your-blog.com/your-article" required=""> <input type="hidden" name="target" value="<%- post.permalink %>>"> <input type="submit" value="Send Webmention"> </form></div> The variable config.title defines the name of your blog with which you have registered at webmention.io. In the hidden input, we use the permalink of the current page/article as the mentions target. Building BridgesYes, the IndieWeb and Webmentions are an alternative concept of social networking, bypassing the big silos like Facebook, Twitter and Co. … but they exist and they have a massive reach. Of course, your posts will be shared there, so why not include those mentions? Bridgy is such a bridge builder. It connects your blog with the big social media players: The only thing you have to do, is to allow Bridgy to access your social media data, like your Tweets on Twitter. If somebody mentions you and one of your articles as a Tweet, Reply or Like, it sends a mention to the endpoint defined in your HTML, in our case webmention.io. Thats it … kinda magic. SummarySome people say, Webmentions makes commenting forms on blogs, like Disqus or others, obsolete, but I don’t agree with that, because not every visitor owns a blog and not every blog owner want’s to write a post only to comment the thoughts of another blogger. Both approaches work well side by side and complement each other. Webmentions are super to build a blog network and increasing your blogs coverage. As I said in Part 1 … we write for readers. More Info indieweb.org: webmention.ioMax Böck: Using Webmentions in EleventyMax Böck: Webmention AnalyticsPaul Kinlan: Using Web Mentions in a static site (Hugo)Sia Karamalegos: An In-Depth Tutorial of Webmentions + EleventyKeith J. Grant: Adding Webmention Support to a Static SiteChris Bongers: Goodbye comments, welcome Webmentions","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Share","slug":"Share","permalink":"https://kiko.io/tags/Share/"}]},{"title":"Hexo and the IndieWeb (Sending Webmentions)","subtitle":"Use webmention.app the easy way","series":"IndieWeb","date":"2021-05-08","updated":"2021-05-08","path":"post/Hexo-and-the-IndieWeb-Sending-Webmentions/","permalink":"https://kiko.io/post/Hexo-and-the-IndieWeb-Sending-Webmentions/","excerpt":"This is part two of a blog post that turned out to be a bit too long. Don’t miss Part 1: Hexo and the IndieWeb … After you have created your new Hexo post with hexo new post "My Fancy Post" and spend a couple of minutes/hours/days on writing meaningful text, you publish it by running hexo generate and copying the generated HTML to your server. Next step would be to inform all the blogs you linked to in your now published post, that you have done just that. You want to send Webmentions. Good news: you don’t have to write your own solution to scan your article for external URL’s and sending Webmentions to their creators: Remy Sharp has done that already with his service webmention.app. It supports the long existing Pingbacks too and offers several ways to achieve your goal:","keywords":"part blog post turned bit long dont miss hexo indieweb … created "my fancy post" spend couple minutes/hoursys writing meaningful text publish running generate copying generated html server step inform blogs linked published send webmentions good news write solution scan article external urls sending creators remy sharp service webmentionapp supports existing pingbacks offers ways achieve goal","text":"This is part two of a blog post that turned out to be a bit too long. Don’t miss Part 1: Hexo and the IndieWeb … After you have created your new Hexo post with hexo new post "My Fancy Post" and spend a couple of minutes/hours/days on writing meaningful text, you publish it by running hexo generate and copying the generated HTML to your server. Next step would be to inform all the blogs you linked to in your now published post, that you have done just that. You want to send Webmentions. Good news: you don’t have to write your own solution to scan your article for external URL’s and sending Webmentions to their creators: Remy Sharp has done that already with his service webmention.app. It supports the long existing Pingbacks too and offers several ways to achieve your goal: (1) Use the service at https://webmention.app/test manually with your newly published article URLVery time consuming approach and actually only intended for tests. You rely on Remy’s service and need a token to avoid existing rate limits. (2) Perform a POST of your new article URL or the URL of your RSS feed to his service endpoint using CURL or any other HTTP requesting methodCan be integrated at the end of your build & deploy process. Rely on Remy’s service and needs a token. (3) Use IFTTT to do the job described in (2) using your feedSeems best way for ‘set up and forget’, but you rely not only on Remy’s service, but on IFTTT also. Not really controllable. (4) Use his Command line tool independentlyUse the executable behind Remy’s service on your own, without tokens and stuff. Integrable into your build & deploy process and the source code is available on Github, in case you want to change it to your needs. 12345678REM Installnpm install @remy/webmentionREM Run test locallynpx webmention https://localhost:4000/my-fancy-post --debugREM Run send with your published URLnpx webmention https://my-blog.com/my-fancy-post --send --limit 0 Whatever method you use: If you use your RSS feed for scanning, you have to ensure, that your feeds content contains the complete text of your article and not only the excerpt, in case you work with Hexo’s <!-- more --> feature. Sending Hexo-StyleTo avoid having to look up the published URL each time, I wrote a Hexo console command that either processes a post by its filename (slug) or simply the last ones. It is based on and uses Remy’s NPM package. You can install hexo-console-webmention by executing: npm install hexo-console-webmention --save The command has 3 options: Parameter Type Description slug string Parse a particular post by its filename (slug) count int Parse a number of latest posts (not considered when –slug is used)>; default = 1 send bool Parse and send Webmentions (without, only the endpoints found are displayed) ExamplesParse and show endpoints for the latest post: 1hexo webmention Parse and show endpoints for the latest 20 posts: 1hexo webmention --count 20 Parse and send Webmentions for the latest post: 1hexo webmention --send true Parse and send Webmentions for the post “My Fancy Blogpost”: 1hexo webmention --slug my-fancy-blogpost --send true Sample Output The source code is available at: https://github.com/kristofzerbe/hexo-console-webmention. Do not linger … this way to Part 3: Hexo and the IndieWeb (Receiving Webmentions) … More Info Paul Kinlan: Webmention.appRemy Sharp: Send Outgoing WebmentionsGithub: Remy Sharp: Source Code from webmention.app","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Share","slug":"Share","permalink":"https://kiko.io/tags/Share/"}]},{"title":"Hexo and the IndieWeb","subtitle":"Make your blog ready for social interaction via Webmentions","series":"IndieWeb","date":"2021-05-05","updated":"2021-05-05","path":"post/Hexo-and-the-IndieWeb/","permalink":"https://kiko.io/post/Hexo-and-the-IndieWeb/","excerpt":"Posted on IndieNews It is cool to publish your thoughts on your own blog under your only domain and not only on big social media platforms, because that way you keep control over your content. But what makes Facebook, Twitter and others “social” is the interaction between the people. Likes, Retweets, Mentions, Replies are the fuel which drives them. But most of the blogging solutions offers only rudimentary interactions, in form of article comments. The comment hurdle is high because interacting on someone else’s site is different from interacting on what is supposed to be your own, such as your Twitter or Facebook feed. The project IndieWeb and their approach of Webmentions, has the goal to fill this gap. As a W3C recommendation, it defines standards how the social interaction of independent blogging solutions can be technically implemented without the need of manual intervention. Let software do the job… In this article I will only briefly go into the basics and then show an implementation solution for the SSG Hexo.","keywords":"posted indienews cool publish thoughts blog domain big social media platforms control content makes facebook twitter interaction people likes retweets mentions replies fuel drives blogging solutions offers rudimentary interactions form article comments comment hurdle high interacting elses site supposed feed project indieweb approach webmentions goal fill gap w3c recommendation defines standards independent technically implemented manual intervention software job… briefly basics show implementation solution ssg hexo","text":"Posted on IndieNews It is cool to publish your thoughts on your own blog under your only domain and not only on big social media platforms, because that way you keep control over your content. But what makes Facebook, Twitter and others “social” is the interaction between the people. Likes, Retweets, Mentions, Replies are the fuel which drives them. But most of the blogging solutions offers only rudimentary interactions, in form of article comments. The comment hurdle is high because interacting on someone else’s site is different from interacting on what is supposed to be your own, such as your Twitter or Facebook feed. The project IndieWeb and their approach of Webmentions, has the goal to fill this gap. As a W3C recommendation, it defines standards how the social interaction of independent blogging solutions can be technically implemented without the need of manual intervention. Let software do the job… In this article I will only briefly go into the basics and then show an implementation solution for the SSG Hexo. Basic ConceptsNothing describes the flow of Webmentions better than this: Frankie posts a blog entry. Alex has thoughts in response, so also posts a blog entry linking to Frankie’s. Alex’s publishing software finds the link and fetches Frankie’s post, finding the URL of Frankie’s Webmention endpoint in the document. Alex’s software sends a notification to the endpoint. Frankie’s software then fetches Alex’s post to verify that it really does link back, and then chooses how to display the reaction alongside Frankie’s post. --- Drew McLellan Basically Webmentions allow notifications between web addresses, therefore every post, which is part of the interaction, has to have a unique permalink. A blog software that wants to support webmentions must cover 4 main points: The HTML has to tell others who you are The HTML has to give dedicated informations about your posts Sending a message to another blog, in case you mentioned one of its posts Reveiving messages from other blogs, in case they mentioned one of your posts Point 4 is probably the most interesting for all of us, because it pats our own ego on the back, since we usually don’t write for ourselves, but for others, and reactions to it, show us that it wasn’t pointless. Step 1: The Personal & Profile HTMLAs you want to interact with other blogs participating in the IndieWeb with your posts, they have to know something about you and your articles in a machine-readable form. Personal InformationHTML is machine-readable per se, but you have to tell others what to look for by adding defined classes to the tags which holds the information, in order to enable them to get specific information about you, like your name, your mail-address or links to other profiles f.e. Github, Twitter and so on. It is necessary to have this information not only in an ABOUT page, but also on each post page. You can achieve this either by having an ABOUT block like here on kiko.io or providing the information in hidden HTML tags elsewhere in your HTML. It does not matter which tags you use, you only have to add the defined class to the tag of a particular information in your Hexo EJS file. The information will be extracted out of the tag’s inner text. The most used classes for personal blogs as follows: Class Information h-card Wrapper for all personal information. All other classes below has to be used on child tags p-name Full name u-email Email address u-photo Photo p-role Role u-url URL representing the person p-locality City or Town p-region State or province p-country-name Country name Please keep in mind not to give too much information about you to the public. It could get unpleasant… Profile InformationFor providing links to other profiles, anchor (A) tags with the special attribute rel="me" will be used, which indicates profile equivalence and can be used for identity-consolidation. With this extension of your blog HTML, you are able to sign in using your domain at sites which supports Web Sign-In over the concept of RelMeAuth, for example those who use IndieAuth.net - OAuth for the open web. You only have to make sure, that the endpoints of your profile links have backlinks to your blog with a rel="me“. Unfortunately, not many services offer the definition of such a backlink. Github, for example, is an exception. You can give it a try at IndieAuth.com. Example Step 2: The Article HTMLTagging articles with meta information for the IndieWeb is similarly simple, by adding following classes in your article.ejs file: Class Information h-entry Wrapper for all article related information p-name Title p-summary Short summary e-content Content dt-published Publish date dt-updated Update date u-url Permalink In case you work with the default Hexo theme ‘landscape’, I advise you to split your article.ejs in two files, because it is used for the article itself and for the excerpts on the start page and archive pages also. I have made an excerpt.ejs with all the information needed for listing the posts and cut back my article.ejs to the bare minimum, but with the IndieWeb related classes above (or in the linked partials if necessary), because only the article page itself should have these informations, respectively an h-entry class, to indicate that there are IndieWeb data! layout/_partial/article.ejs123456789101112131415161718192021222324252627282930313233343536<article id="<%= post.layout %>-<%= post.slug %>" class="article article-type-<%= post.layout %> h-entry" itemscope itemprop="blogPost"> <div class="article-meta"> <div class="h-card p-author" style="display:none"> <img class="u-photo" src="<%- config.photo %>" alt="<%- config.author %>" /> <a class="p-name u-url" href="<%- config.url %>" rel="author"><%- config.author %></a> </div> <%- partial('post/date', { class_name: 'article-date dt-published', date_format: 'DD MMM YYYY' }) %> <%- partial('post/category', { class_name: 'article-category p-category' }) %> </div> <div class="article-inner"> <header class="article-header"> <%- partial('post/title', { class_name: 'article-title p-name', show_link: false }) %> <%- partial('post/subtitle', { class_name: 'article-subtitle p-summary' }) %> </header> <div class="article-entry e-content" itemprop="articleBody"> <%- post.content %> </div> <footer class="article-footer"> <%- partial('post/tag', { class_name: 'article-tags' }) %> <%- partial('post/permalink', { class_name: 'article-permalink u-url' }) %> </footer> </div> <%- partial('post/comments') %> <%- partial('post/related') %> <%- partial('post/nav') %></article> External LinksThe Interaction with other blogs takes place through linking to those external sources in the content of your article. Lets say you want to write about a specific topic and to mention the work of another developer, then you just place a link to his post in your Markdown, as you have been doing all along: /source/_posts/my-fancy-post.md1234# My Fancy Post...Jack has done a wonderful job with his [Awesome Work](https://jacks-blog.com/awesome-work)... It will be transformed while generating into something like that: /output/.../my-fancy-post/index.html123456789101112131415161718192021<body> ... <article class="h-entry"> ... <div class="article-inner"> <header class="article-header"> <h1 class="p-name">My Fancy Post</h1> </header> <div class="article-entry e-content"> ... <p> Jack has done a wonderful job with his <a href="https://jacks-blog.com/awesome-work">Awesome Work</a>. </p> ... </div> ... </div> ... </article> ...</body> In the terms of the IndieWeb concept, your post will a be an article, which mentions other posts, as the old-fashioned pingbacks do. Special Post FormatsA true interaction takes place, when you are posting in a certain syndication context … with a note as a response to the work of others, mainly by adding additional classes to the external link: u-in-reply-to … to indicate that your post is a reply to a post as part of a conversation u-like-of … to indicate that your post is a like u-repost-of … to indicate that your post is a repost (100% re-publication) u-bookmark-of … to indicate that your post is a bookmark Every response type can have additional information about your post and the syndication of it. Example REPLY12345678910111213<body> ... <div class="h-entry"> <p> In reply to: <a class="u-in-reply-to" href="https://jacks-blog.com/awesome-work">Jacks Blog: Awesome Work</a> </p> <p class="p-name e-content"> Jack, you have done a wonderful job! </p> ... </div> ...</body> Example LIKE12345678910<body> ... <div class="h-entry"> <p class="p-summary"> Kristof liked <a class="u-like-of" href="https://jacks-blog.com/awesome-work">Jacks Awesome Work at https://jacks-blog.com/awesome-work</a> </p> ... </div> ...</body> Currently, I would not recommend writing responses as a normal post in Hexo, as it is based on structured text, that best describes the IndieWeb concept of an ARTICLE.As this post is part of a new series called IndieWeb, I will post a solution for responses is the near future. VerificationTo check all your changes, you can use IndieWebify.Me (Level 1 & 2): This was supposed to be just one post, but it got longer and longer and so I split it into 3 parts. Don’t miss Part 2: Hexo and the IndieWeb (Sending Webmentions) … TerminologyThere are a lot of posts out there which explains the basic concepts of the IndieWeb and Webmentions in particular and you will stumble upon some terms, which has to be explained: Personal Domain … is a domain name that you personally own, control, and use to represent yourself on the internet. Getting a personal domain is the first step towards getting on the indieweb, and is therefore a requirement for IndieMark Level 1 --- indieweb.org (Personal Domain) Microformats … are small patterns of HTML to represent commonly published things like people, events, blog posts, reviews and tags in web pages. They are the quickest & simplest way to provide an API to the information on your website. --- microformats.org (Wiki) POSSE … is an abbreviation for Publish (on your) Own Site, Syndicate Elsewhere, the practice of posting content on your own site first, then publishing copies or sharing links to third parties (like social media silos) with original post links to provide viewers a path to directly interacting with your content. --- indieweb.org (POSSE) Backfeed … is the process of syndicating interactions on your POSSE copies back (AKA reverse syndicating) to your original posts. --- indieweb.org (Backfeed) Web sign-in … is signing in to websites using your personal web address (without having to use your e-mail address). Web sign-in supersedes OpenID. --- indieweb.org (Web sign-in) RelMeAuth … is a proposed open standard for using rel=”me” links to profiles on oauth supporting services to authenticate via either those profiles or your own site. RelMeAuth is the technology behind Web sign-in. --- microformats.org (RelMeAuth) IndieAuth … is a federated login protocol for Web sign-in, enabling users to use their own domain to sign in to other sites and services. --- indieweb.org (IndieAuth) More Info A List Apart: Webmentions: Enabling Better Communication on the Internetindieweb.org: Getting Startedindieweb.org: How to set up web sign-in on your own domainindieweb.org: IndieWeb ExamplesBryce Wray: Webmentions in three SSGs: Part 1Keith J. Grant: Adding Webmention Support to a Static SiteAlessio Caiazza: Articles tagged ´indieweb´* (Forum): Anyone for Webmention?","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Share","slug":"Share","permalink":"https://kiko.io/tags/Share/"}]},{"title":"Triangulate your images with Triangula","subtitle":null,"series":"Great Finds","date":"2021-04-30","updated":"2021-04-30","path":"post/Triangulate-your-images-with-Triangula/","permalink":"https://kiko.io/post/Triangulate-your-images-with-Triangula/","excerpt":"As I am a photo enthusiast I’m always excited to find new tools, to give images a unique look. Today I stumbled over Triangula. Ever seen one of those cool backgrounds, where a picture has been broken up into lots of little triangles? In trigonometry and elementary geometry, the division of a surface into triangles is called a triangular grid, triangular mesh or triangulation.Wikipedia Whoever RH12503 (Ryan H??) is, he did an amazing job on creating this little Go program, including a pleasing UI, do convert images into those equivalents.","keywords":"photo enthusiast im excited find tools give images unique today stumbled triangula cool backgrounds picture broken lots triangles trigonometry elementary geometry division surface called triangular grid mesh triangulationwikipedia rh12503 ryan amazing job creating program including pleasing ui convert equivalents","text":"As I am a photo enthusiast I’m always excited to find new tools, to give images a unique look. Today I stumbled over Triangula. Ever seen one of those cool backgrounds, where a picture has been broken up into lots of little triangles? In trigonometry and elementary geometry, the division of a surface into triangles is called a triangular grid, triangular mesh or triangulation.Wikipedia Whoever RH12503 (Ryan H??) is, he did an amazing job on creating this little Go program, including a pleasing UI, do convert images into those equivalents. These images are absolute great for background images in websites, in order to make the details less recognisable. There is a web version of Triangula, but the desktop version (including a console version) is much faster. Best feature is the ability not only to save the generated images as PNG, but also as SVG! Example with 1.000 Generations var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-odfcfj\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Triangulated', onHover: true, } }).mount(); Example with 10.000 Generations var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-s69q0q\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Triangulated', onHover: true, } }).mount(); Example with 20.000 Generations var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-stjur1\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Triangulated', onHover: true, } }).mount();","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"SVG","slug":"SVG","permalink":"https://kiko.io/tags/SVG/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"Forking Hexo plugin 'hexo-index-anything'","subtitle":"Introducing its successor 'hexo-generator-anything'","date":"2021-04-25","updated":"2021-04-25","path":"post/Forking-Hexo-plugin-hexo-index-anything/","permalink":"https://kiko.io/post/Forking-Hexo-plugin-hexo-index-anything/","excerpt":"As I started with this blog 2 years ago, I wanted to document the customization of Hexo to my needs in a series of articles. To group these articles I considered using the build-in categories, but I already used them to group articles by the underlying tech stack or area, like ´JavaScript´, ´C#´ or ´Tools´ and I didn’t want to mix it, as the category was also used in the Url of a post. I was researching another grouping solution for Hexo and stumbled upon hexo-index-anything, a very clever Hexo plugin to generate index pages for almost every FrontMatter variable in a post. As it was freely available under a MIT license on Github, I forked it in July 2020 and made some bug fixes and drop a pull request to Levi … but he unfortunately never answered my pull or issue requests and has set the status of the project to DEPRECATED. Ok then … make a successor on your own, fella…","keywords":"started blog years ago wanted document customization hexo series articles group considered build-in categories underlying tech stack area ´javascript´ ´c#´ ´tools´ didnt mix category url post researching grouping solution stumbled hexo-index-anything clever plugin generate index pages frontmatter variable freely mit license github forked july made bug fixes drop pull request levi … answered issue requests set status project deprecated make successor fella…","text":"As I started with this blog 2 years ago, I wanted to document the customization of Hexo to my needs in a series of articles. To group these articles I considered using the build-in categories, but I already used them to group articles by the underlying tech stack or area, like ´JavaScript´, ´C#´ or ´Tools´ and I didn’t want to mix it, as the category was also used in the Url of a post. I was researching another grouping solution for Hexo and stumbled upon hexo-index-anything, a very clever Hexo plugin to generate index pages for almost every FrontMatter variable in a post. As it was freely available under a MIT license on Github, I forked it in July 2020 and made some bug fixes and drop a pull request to Levi … but he unfortunately never answered my pull or issue requests and has set the status of the project to DEPRECATED. Ok then … make a successor on your own, fella… Basic FunctionalityLet me describe how the original plugin and my successor are working in general: Assume, you have several posts from different authors and you want a list of all posts for every author. The only thing you have to do (after installation and configuration of the plugin), is to add a custom variable called author to the FrontMatter of each post, with the name of the author as value. /source/_posts/my-fancy-post.md1234title: My Fancy Postdate: 2021-04-25 13:41:46author: Kristof... Next time you run hexo generate, several new files will be available in your output folder: One INDEX page … for the author index, with a list of all available authors and the number of the posts: /authors/index.html Many POSTS pages …, one for each author for the author index, with all of the authors posts: /authors/kristof/index.html /authors/ … /index.html New Plugin NameIn order to avoid confusion (and be able to provide the plugin on Github/NPM) I needed a new name for the plugin and due to the fact that it is a generator, I named it simply hexo-generator-anything. New FeaturesThe original code was really hard to read, because every second variable was named ‘index’, therefore I started with some refactorings, before I continued implementing new features. ConfigurationThe original, one-dimensional variable to index mapping, wasn’t meaningful enough for me. The new mapping is now a dictionary and has unique identifiers: /_config.yml123456anything: layout_index: anything-index layout_posts: anything-posts index_mappings: - variable: author path: authors The template* setting names has changed into layout_*, to make more clear what they are meant for. They are pointing to an existing EJS file in your Hexo themes layout folder to render the particular page. layout_indexEJS file (without extension) to render the INDEX page layout_PostsEJS file (without extension) to render each POSTS page On Github and in the NPM package you will find sample EJS files and some partials, to take the SoC pattern into account. The original titleSeparator setting is not longer available, because it is not necessary anymore. By providing a data structure for index with the attributes name and caption, a title can be put together in the EJS file itself. Introducing linked Markdown filesOne of my needs was, to provide more information on the INDEX page and the POSTS pages for each entry or, to stay with the example, for each author. As the _posts folder is used for storing the post data files, I introduced a _anything folder for dropping MD files, which are linked to the index and its values. The subfolders of _anything are representing the index and the files in it each possible value of the index. The file is structured like any other post file: the FrontMatter data at the top and below some content to show along with the the entry to describe it: /source/_anything/authors/kristof.md1234567---title: Kristofgithub: https://github.com/kristofzerbeavatar: kristof.png---... some smart things to say about Kristof, or links or images, whatever ... The INDEX file itself may have its own Markdown file, to provide addition text or data to it, like describing the list. The variable title is a requirement in the Markdown files, but you can add as many variables as you need and use it in the EJS files. All data will be passed through to the template. ConclusionThe new plugin runs very well and it is heavy in use on generating the sections Series and Projects of this blog. Give it a try …","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"}]},{"title":"Discoveries #9","subtitle":null,"series":"Discoveries","date":"2021-04-20","updated":"2021-04-20","path":"post/Discoveries-9/","permalink":"https://kiko.io/post/Discoveries-9/","excerpt":"Todays issue is all about extending your Web Developers toolbox with some useful libraries to provide the best UX to your users or visitors. Tables, Dropdowns, Color Pickers, Footnotes and GDPR dialogs on steroids. JSTableLuckysheetVirtual SelectLC SelectVanilla ColorfulDuet Date PickerCookie ThoughFull-Screen-Touch-SliderBigfootNumber Rollup","keywords":"todays issue extending web developers toolbox libraries provide ux users visitors tables dropdowns color pickers footnotes gdpr dialogs steroids jstableluckysheetvirtual selectlc selectvanilla colorfulduet date pickercookie thoughfull-screen-touch-sliderbigfootnumber rollup","text":"Todays issue is all about extending your Web Developers toolbox with some useful libraries to provide the best UX to your users or visitors. Tables, Dropdowns, Color Pickers, Footnotes and GDPR dialogs on steroids. JSTableLuckysheetVirtual SelectLC SelectVanilla ColorfulDuet Date PickerCookie ThoughFull-Screen-Touch-SliderBigfootNumber Rollup JSTable by Tobias Hägenläuer https://github.com/Trekky12/JSTable JSTable is a library to convert a static HTML TABLE element into an interactive and responsive one, which supports paging, sorting and searching. Luckysheet by MengShu Open Source https://github.com/mengshukeji/Luckysheet Luckysheet is an Excel or Google Sheets clone, for using in your own web projects. It is nearly as powerfull as his role models, but Open Source. Supports imports and has a plugin interface. Virtual Select by Sa Si Dev https://sa-si-dev.github.io/virtual-select/#/ Whoever ‘Sa Si Dev’ is … he/she made a replacement for the ordinary HTML SELECT element, which is nearly unrivalled. It supports search, matched term marking, multi-select, disabling options, option groups, adding new options and many others. One of the best features, is the dialog style select on mobile devices. LC Select by Luca https://lcweb.it/lc-select-javascript-plugin Luca from Italy made this excellent SELECT replacement, which can show the selected options as pills or as grouped lists with images. It supports a search bar, light/dark theme, multilanguage and is mobile ready as well as it has full keyboard support. Vanilla Colorful by Serhii Kulykov https://web-padawan.github.io/vanilla-colorful/ This color picker is just awesome. It looks really pleasing and has all the features you expect. It is written in TypeScript and authored using native ES modules, without dependencies. Duet Date Picker by Duet Design System https://duetds.github.io/date-picker/ This clean looking date picker is easy to integrate and supports mostly everything you can expect: keyboard, mobile devices and even screen readers. Cookie Though by In The Pocket https://cookiethough.dev/ Since the GDPR has hit the industry, there are tons of solutions to show the necessary consent dialog. A team from Belgium has made a really nice one, without misleading buttons, annoying full screen modes or other dark patterns. And it is Open Source… Full-Screen-Touch-Slider by Will Adams https://github.com/bushblade/Full-Screen-Touch-Slider Will has created a really simple, but good looking and animated full screen touch slider, which works great on mobile devices. The only question is: Why is he so addicted to knifes? Bigfoot by Chris Sauve http://www.bigfootjs.com/ Chris from Canada has a smart solution for showing footnotes in HTML documents: show them as bubbles above the text by clicking a tiny button, which replaces the orginial footnote. Only drawback: it’s a jQuery plugin… Number Rollup by marknorrapscm https://marknorrapscm.github.io/number-rollup/ Number Rollup does what the name suggests: it rollups a number from a starting to an end point, animated and customizable. Nice.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Scotch Presets for Lightroom","subtitle":null,"series":"Lightroom Presets","date":"2021-04-18","updated":"2021-04-18","path":"post/Scotch-Presets-for-Lightroom/","permalink":"https://kiko.io/post/Scotch-Presets-for-Lightroom/","excerpt":"Have you ever been in Scotland during summertime? Wonderful tranquility, great scenery and great photo opportunities at every turn. In 2019 I’ve made some great shots there and I want to share the presets with you, that I have developed for processing the photos from my trip through the Highlands and the Isle of Skye.","keywords":"scotland summertime wonderful tranquility great scenery photo opportunities turn ive made shots share presets developed processing photos trip highlands isle skye","text":"Have you ever been in Scotland during summertime? Wonderful tranquility, great scenery and great photo opportunities at every turn. In 2019 I’ve made some great shots there and I want to share the presets with you, that I have developed for processing the photos from my trip through the Highlands and the Isle of Skye. Scotch LightsSo far in the north the light is totally different. When it shines through the clouds, it hits you. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-dplent\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scotch Lights.xmp Scotch StrengthYou must be made of different stuff if you are Scottish. There is hardly anything fine and graceful about the landscape and the weather. It’s rough and tough and you have to deal with it. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-xzlxvr\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scotch Strength.xmp Scotch SunsetThe view over the North Sea at the edge of the continent is really unique, especially at sunset. Hear the the seagulls scream, the waves crashing against the cliffs and see the stunning colors. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-cl6lse\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scotch Sunset.xmp Scotch EnergyScotland is bold and so full with energy, even in the details. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-qn8hk4\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scotch Energy.xmp Scotch TattooThe Edinburgh Military Tattoo, which takes place twice a year, is a feast for the senses. The incredible sound on one hand and the colors on the other. The brass, the feathers, the uniforms stand out in the setting sun. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-atotju\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scotch Tattoo.xmp","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Presets","slug":"Presets","permalink":"https://kiko.io/tags/Presets/"}]},{"title":"Adding Screenshots to Trello Cards on Android","subtitle":null,"series":"Step By Step","date":"2021-04-11","updated":"2021-04-11","path":"post/Adding-Screenshots-to-Trello-Cards-on-Android/","permalink":"https://kiko.io/post/Adding-Screenshots-to-Trello-Cards-on-Android/","excerpt":"I’m collecting interesting One-Page-Tools on the web on a Trello board. To add a new card, I use a simple little script on my Android smartphone, I wrote about here: Add website to Trello card the better way. On processing the page to store on a card, Trello scrapes the page and takes the <meta> tag og:image out of the HTML to generate an image attachment and take it as cover for the card. This sometimes works, but most of the time it doesn’t, because website owners often don’t pay attention to reasonable <meta> tags. Because it is easier to find a card with visual support, I create my own screenshots for the cards in a manual, but streamlined, process, I want to show you here.","keywords":"im collecting interesting one-page-tools web trello board add card simple script android smartphone wrote website processing page store scrapes takes <meta> tag ogimage html generate image attachment cover works time doesnt owners dont pay attention reasonable tags easier find visual support create screenshots cards manual streamlined process show","text":"I’m collecting interesting One-Page-Tools on the web on a Trello board. To add a new card, I use a simple little script on my Android smartphone, I wrote about here: Add website to Trello card the better way. On processing the page to store on a card, Trello scrapes the page and takes the <meta> tag og:image out of the HTML to generate an image attachment and take it as cover for the card. This sometimes works, but most of the time it doesn’t, because website owners often don’t pay attention to reasonable <meta> tags. Because it is easier to find a card with visual support, I create my own screenshots for the cards in a manual, but streamlined, process, I want to show you here. PrerequisitesOS: Android 5 and above Apps: Trello https://play.google.com/store/apps/details?id=com.trello Screenshot Touch https://play.google.com/store/apps/details?id=com.mdiwebma.screenshot Step 1 Open up Screenshot Touch and set under Shaking and Delay the option Caption by shaking [1]. Set the Shake sensitivity option to Hard [2]. Set the Resize option to 50% [3]. Start the capture monitoring service [4]. Step 2 Open up Trello and got to the card, where you want to add an screenshot for use as an cover. Click on the URL attachment to open it in your browser [1]. Step 3 Shake you smartphone to capture the current website. Step 4 Switch to Screenshot Touch and open the Photo Viewer [1] Step 5 Open the Crop Image dialog. Step 6 Set the Crop Mode to 1:1 to get a squared image. It will be persisted in the apps settings for further use. Step 7 Place the overlay to select you preferred part of the image [1]. Save the image [2]. Step 8 Switch back to Trello and click to add a new attachment [1]. Step 9 Choose to add from file [1]. Step 10 In Androids file dialog, head up to Recent Files [1]. Select the new image [2]. Step 11 In the context menu of the added attachment, select Make Card Cover [1], if Trello has not done it yet. ResultOnce you have done this two or three times, the process takes less than 30 seconds.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Trello","slug":"Trello","permalink":"https://kiko.io/tags/Trello/"},{"name":"Android","slug":"Android","permalink":"https://kiko.io/tags/Android/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"SVG Resources","subtitle":null,"series":"Great Finds","date":"2021-04-09","updated":"2021-04-09","path":"post/SVG-Resources/","permalink":"https://kiko.io/post/SVG-Resources/","excerpt":"#post-SVG-Resources button { background-color: #f1f1f1; border: none; padding: 1rem; margin-bottom: 1rem; margin-right: 1rem; cursor: pointer; } #post-SVG-Resources em { opacity: 0.33; } #post-SVG-Resources #info { color: silver; display: block; height: 24px; } #post-SVG-Resources #info.result { color: green; font-weight: bold; } var timeoutID; function setInfo(e,m) { let info = document.getElementById(e); info.textContent = m; info.classList.add(\"result\"); window.clearTimeout(timeoutID); timeoutID = setTimeout(function() { info.textContent = \"Guess and click...\"; info.classList.remove(\"result\"); window.clearTimeout(timeoutID); }, 2000); } Since beginning beginning of time, people are using symbols to make things clear quickly and easily. So do we when developing websites and web apps by using icons. Everybody knows what’s behind a loupe symbol or a hamburger icon. Guess and click... The way we implement icons have changed in the past. From BMP files to GIF and JPG files, PNG files, to complete or customizable symbol fonts like fontello.com, to Scalable Vector Graphics (SVG). SVG’s in particular are becoming increasingly popular, because they are nothing more than XML-like code, that can be manipulated via CSS or JS, their digital footprint is unbeatable small and they scale seemlessly. Dealing with SVG’s is a little bit more difficult than placing a PNG in HTML, because of its complexity, but it is worth learning as much as possible about it. So did I in the last couple of month and I want to share my finds on the web with you in this post.","keywords":"#post-svg-resources button { background-color #f1f1f1 border padding 1rem margin-bottom margin-right cursor pointer } em opacity #info color silver display block height 24px #inforesult green font-weight bold var timeoutid function setinfoem info = documentgetelementbyide infotextcontent infoclasslistaddresult windowcleartimeouttimeoutid settimeoutfunction guess click infoclasslistremoveresult beginning time people symbols make things clear quickly easily developing websites web apps icons whats loupe symbol hamburger icon implement changed past bmp files gif jpg png complete customizable fonts fontellocom scalable vector graphics svg svgs increasingly popular xml-like code manipulated css js digital footprint unbeatable small scale seemlessly dealing bit difficult placing html complexity worth learning couple month share finds post","text":"#post-SVG-Resources button { background-color: #f1f1f1; border: none; padding: 1rem; margin-bottom: 1rem; margin-right: 1rem; cursor: pointer; } #post-SVG-Resources em { opacity: 0.33; } #post-SVG-Resources #info { color: silver; display: block; height: 24px; } #post-SVG-Resources #info.result { color: green; font-weight: bold; } var timeoutID; function setInfo(e,m) { let info = document.getElementById(e); info.textContent = m; info.classList.add(\"result\"); window.clearTimeout(timeoutID); timeoutID = setTimeout(function() { info.textContent = \"Guess and click...\"; info.classList.remove(\"result\"); window.clearTimeout(timeoutID); }, 2000); } Since beginning beginning of time, people are using symbols to make things clear quickly and easily. So do we when developing websites and web apps by using icons. Everybody knows what’s behind a loupe symbol or a hamburger icon. Guess and click... The way we implement icons have changed in the past. From BMP files to GIF and JPG files, PNG files, to complete or customizable symbol fonts like fontello.com, to Scalable Vector Graphics (SVG). SVG’s in particular are becoming increasingly popular, because they are nothing more than XML-like code, that can be manipulated via CSS or JS, their digital footprint is unbeatable small and they scale seemlessly. Dealing with SVG’s is a little bit more difficult than placing a PNG in HTML, because of its complexity, but it is worth learning as much as possible about it. So did I in the last couple of month and I want to share my finds on the web with you in this post. Using SVG’s in briefThe most useful way of using SVG’s is as an image out of a file, either directly …: (use DevTools [F12] to inspect the element) 1<img src="images/options.svg" /> … or as a background image: button.options { height: 56px; width: 56px; background-image: url(options.svg); background-repeat: no-repeat; background-position: 50% 50%; } (use DevTools [F12] to inspect the element) 1234567891011<style> button.options { height: 56px; width: 56px; background: url(images/options.svg); background-repeat: no-repeat; background-position: 50% 50%; }</style><button class="options"></button> As files, no matter how small, has to be requested from the server, you can also define SVG’s inline for better performance: (use DevTools [F12] to inspect the element) 12345678910111213<body> ... <button> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"> <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M7 3C8.86384 3 10.4299 4.27477 10.874 6H19V8H10.874C10.4299 9.72523 8.86384 11 7 11C4.79086 11 3 9.20914 3 7C3 4.79086 4.79086 3 7 3ZM7 9C8.10457 9 9 8.10457 9 7C9 5.89543 8.10457 5 7 5C5.89543 5 5 5.89543 5 7C5 8.10457 5.89543 9 7 9Z" /> <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M17 20C15.1362 20 13.5701 18.7252 13.126 17H5V15H13.126C13.5701 13.2748 15.1362 12 17 12C19.2091 12 21 13.7909 21 16C21 18.2091 19.2091 20 17 20ZM17 18C18.1046 18 19 17.1046 19 16C19 14.8954 18.1046 14 17 14C15.8954 14 15 14.8954 15 16C15 17.1046 15.8954 18 17 18Z" /> </svg> </button> ...</body> If you want to use a SVG multiple times, you can define it once by wrapping it up in a symbol tag with an id and use it wherever you want: (use DevTools [F12] to inspect the elements) 12345678910111213141516171819<body> <svg xmlns="http://www.w3.org/2000/svg"> <symbol id="options" width="24" height="24" viewBox="0 0 24 24" fill="none"> <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M7 3C8.86384 3 10.4299 4.27477 10.874 6H19V8H10.874C10.4299 9.72523 8.86384 11 7 11C4.79086 11 3 9.20914 3 7C3 4.79086 4.79086 3 7 3ZM7 9C8.10457 9 9 8.10457 9 7C9 5.89543 8.10457 5 7 5C5.89543 5 5 5.89543 5 7C5 8.10457 5.89543 9 7 9Z" /> <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M17 20C15.1362 20 13.5701 18.7252 13.126 17H5V15H13.126C13.5701 13.2748 15.1362 12 17 12C19.2091 12 21 13.7909 21 16C21 18.2091 19.2091 20 17 20ZM17 18C18.1046 18 19 17.1046 19 16C19 14.8954 18.1046 14 17 14C15.8954 14 15 14.8954 15 16C15 17.1046 15.8954 18 17 18Z" /> </symbol> </svg> ... <button> <svg width="24" height="24"><use xlink:href="#options" /></svg> </button> ... <button> <svg width="24" height="24"><use xlink:href="#options" /></svg> </button> ...</body> SVG ResourcesFinding the right SVG for your project is time consuming, like it is for symbol fonts or PNG’s. So here are a few tips getting SVG’s for free: css.gg https://css.gg 700+ icons, downloadable as SVG, PNG, XD, Figma, Styled Component (Typescript) or even pure CSS. Tabler Icons https://tabler-icons.io Over 1.250 icons in several categories, downloadable as SVG or PNG. Boxicons https://boxicons.com 1.500 regular or filled icons, downloadable as SVG or PNG. Supports animations, Web Components and is also available as font. Feather https://feathericons.com 268 icons as SVG, with customizable size, stroke with and color. Majesticons https://majesticons.com 210 line and solid icons, with Figma support and also available as Github repository. Simple icons https://simpleicons.org Over 1800 icons of popular brands, with hex color code. SVG Repo https://www.svgrepo.com 300.000+ vectors and icons in over 400 collections from different artists. Google Fonts - Material Icons https://fonts.google.com/icons At least … an own frontend of Googles Material Icons inside Google Fonts for downloading them individually as SVG, PNG or Android/iOS package. Last but not least, SVG is more powerful then drawing stuff. It’s possible to add raster images, text with a particular font and use many CSS-like techniques like gradients and animations. See links below… More Info CSS Tricks: Using SVGCSS-Tricks: Use and Reuse Everything in SVG… Even Animations!CSS Tricks: An SVG That Isn’t All… SVGmediaevent.de: Sieben Wege, SVG in HTML-Seiten zu setzen (German)Foxland: Simple and Accessible SVG Menu Hamburger Animation","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"SVG","slug":"SVG","permalink":"https://kiko.io/tags/SVG/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"Discoveries #8","subtitle":null,"series":"Discoveries","date":"2021-03-31","updated":"2021-03-31","path":"post/Discoveries-8/","permalink":"https://kiko.io/post/Discoveries-8/","excerpt":"This month my discoveries are all about CSS … at least almost. See the stunning solutions developers around the world have created and take them to improve yours. Have fun exploring. Charts.cssAnimXYZMagic Animationstransition.cssMake Animated Content Placeholders with HTML and CSSAnimating UnderlinesNew aspect-ratio CSS propertyHow to display language-specific quotes in CSSMaking the DETAILS element look and behave like a modalBetter Line Breaks for Long URLs","keywords":"month discoveries css … stunning solutions developers world created improve fun exploring chartscssanimxyzmagic animationstransitioncssmake animated content placeholders html cssanimating underlinesnew aspect-ratio propertyhow display language-specific quotes cssmaking details element behave modalbetter line breaks long urls","text":"This month my discoveries are all about CSS … at least almost. See the stunning solutions developers around the world have created and take them to improve yours. Have fun exploring. Charts.cssAnimXYZMagic Animationstransition.cssMake Animated Content Placeholders with HTML and CSSAnimating UnderlinesNew aspect-ratio CSS propertyHow to display language-specific quotes in CSSMaking the DETAILS element look and behave like a modalBetter Line Breaks for Long URLs Charts.css by Rami Yushuvaev and Lana Gordiievski https://chartscss.org Mentioned in hundreds of other blog posts earlier, Chart.css is so good that I have to mention it here too. Pure CSS charts, with animations, responsiveness, customizable and Open Source … what more could a heart desire? AnimXYZ by Miles and Mattan Ingram https://animxyz.com AnimXYZ is a CSS library for composing animations, powered by CSS variables. It has Vue and React support. Magic Animations by Christian Pucci https://www.minimamente.com/project/magic Christian from Italy brings us an animation library with 64 beautiful effects, to get started directly. transition.css by Adam Argyle https://github.com/argyleink/transition.css Another CSS library for animating things on the web. Adam is targeting the transition of an element. Cool and easy to use. Make Animated Content Placeholders with HTML and CSS by James Sinkala https://dev.to/xinnks/make-animated-content-placeholders-with-html-and-css-3ekn A modern approach to entertain web users while loading some content or images is to show animated placeholders, like Instagram, Facebook and others do. James gives us the instructions how to implement these with pre CSS. Animating Underlines by Michelle Barker https://css-irl.info/animating-underlines From the beginning of time, URL’s has shown as underlined text. How boring. Michelle has some ideas to bring some life into links. New aspect-ratio CSS property by Una Kravets https://web.dev/aspect-ratio Dealing with images in CSS can be a mess sometimes, especially on responsive layouts. Read about the common hacks regarding aspect ratio and the upcoming new CSS feature aspect-ratio. How to display language-specific quotes in CSS by Stefan Judis https://www.stefanjudis.com/today-i-learned/how-to-use-language-dependent-quotes-in-css Doing internationalization right, you have to beware of some pitfalls, like the different quotes in some languages. Germans are using different double quotes for start and end, French are using double arrows and so on. Stefan shows us how to do it right. Making the DETAILS element look and behave like a modal by Niels Voogt https://codepen.io/NielsVoogt/full/XWjPdjO In this pen, Niels is playing around with the DETAILS tag and shows how it can be used for a modal dialog with CSS only. Great idea! Better Line Breaks for Long URLs by Reuben Lillie https://css-tricks.com/better-line-breaks-for-long-urls Reuben addresses in his post at CSS-Tricks the problem of displaying long URL’s and shows a solution with a little bit JavaScript how to do it right once and for all.","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Custom Caller Authentication with ASP.NET Core 5.0 Web API","subtitle":null,"date":"2021-02-28","updated":"2021-02-28","path":"post/Custom-Caller-Authentication-with-ASP-NET-Core-5-0-WebApi/","permalink":"https://kiko.io/post/Custom-Caller-Authentication-with-ASP-NET-Core-5-0-WebApi/","excerpt":"Developing micro services with Microsoft ASP.NET Core 5.0 Web API is powerful and fun, but the fun stops, if your data are accesses unauthorized. It is absolutely fundamental to have a protection layer, which filters out unwanted data requests. A common way is to limit the service access by providing API Keys to well known clients. In this post I will show you how to implement such a filter in terms of API keys and IP addresses.","keywords":"developing micro services microsoft aspnet core web api powerful fun stops data accesses unauthorized absolutely fundamental protection layer filters unwanted requests common limit service access providing keys clients post show implement filter terms ip addresses","text":"Developing micro services with Microsoft ASP.NET Core 5.0 Web API is powerful and fun, but the fun stops, if your data are accesses unauthorized. It is absolutely fundamental to have a protection layer, which filters out unwanted data requests. A common way is to limit the service access by providing API Keys to well known clients. In this post I will show you how to implement such a filter in terms of API keys and IP addresses. The SettingsLets start with the list of clients, who should be able to access the data. The most useful place for this is in the appsettings.json of the Core 5.0 Web API project: appsettings.json123456789101112131415..."Callers": [ { "Name": "localhost", "ApiKey": null, "IPAddress": "::1" }, { "Name": "John Doe", "ApiKey": "mytopsecretapikeyforjohndoe", "IPAddress": "*" } ]... This list has two entries: one for the server itself (“localhost”), which is restricted to the local IP address "::1", and one for the test user "John Doe", who can access from any IP address ("*"), but must supply his personal API key with his requests. In order to handle this setting, we have to introduce it to the system at startup as a class: CallerSetting.cs123456public class CallerSetting{ public string Name { get; set; } public string ApiKey { get; set; } public string IPAddress { get; set; }} Startup.cs1234567891011...public void ConfigureServices(IServiceCollection services) { ... IConfigurationSection configSection = Configuration.GetSection("Callers"); services.Configure<List<CallerSetting>>(configSection);}... The ControllerLet’s assume we have a controller, which handles the API requests, like this: MyFancyController.cs123456789101112131415using Microsoft.AspNetCore.Mvc;namespace MyAPIProject{ [Route("api/helloworld")] [ApiController] public class MyFancyAPIController : ControllerBase { [HttpGet] public string Get() { return "Hello World"; } }} To prevent to write a request check against our new settings in each action method, we can decorate the whole controller class by introducing an new custom Attribute, which will do the work: MyFancyController.cs1234567...[Route("api/helloworld")][ApiController][AuthenticateApiRequest]public class MyFancyAPIController : ControllerBase... The AttributeHere is the code for the new attribute. It uses the IActionFilter. These filters run within the ASP.NET Core action invocation pipeline, in our case BEFORE the action is entered (OnActionExecutionAsync). AuthenticateApiRequestAttribute.cs1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859using Microsoft.AspNetCore.Mvc;using Microsoft.AspNetCore.Mvc.Filters;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;using System;using System.Collections.Generic;using System.Threading.Tasks;using System.Linq;namespace MyAPIProject{ [AttributeUsage(AttributeTargets.Class)] public class AuthenticateApiRequestAttribute : Attribute, IAsyncActionFilter { public async Task OnActionExecutionAsync ( ActionExecutingContext context, ActionExecutionDelegate next ) { // Get an Api Key from Request Header context.HttpContext.Request.Headers.TryGetValue( "ApiKey", out var requestApiKey ); // Get the remote IP Address var requestIpAddress = context.HttpContext.Connection.RemoteIpAddress.ToString(); // Get access to 'appsettings.json' var appSettings = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>(); // Get 'Callers' list from settings var callers = appSettings.GetSection("Callers") .Get<List<CallerSetting>>(); // Get all Callers with matching IP Adress and/or API Key via LINQ var current = callers.Where(c => (c.IPAddress == requestIpAddress) || (c.IPAddress == "*" && c.ApiKey == requestApiKey) || (c.IPAddress == requestIpAddress && c.ApiKey == requestApiKey)); // Do we have a match? if (current.Count() == 0) { // No, then return with an error context.Result = new ContentResult() { StatusCode = 401, Content = "Unauthorized Access" }; return; } await next(); } }} The Result More Info Microsoft Docs: Filters in ASP.NET CoreMicrosoft Docs: Configuration in ASP.NET Core","categories":[{"name":".NET","slug":"NET","permalink":"https://kiko.io/categories/NET/"}],"tags":[{"name":"Visual Studio","slug":"Visual-Studio","permalink":"https://kiko.io/tags/Visual-Studio/"},{"name":"WebAPI","slug":"WebAPI","permalink":"https://kiko.io/tags/WebAPI/"},{"name":"Authentication","slug":"Authentication","permalink":"https://kiko.io/tags/Authentication/"}]},{"title":"Discoveries #7","subtitle":null,"series":"Discoveries","date":"2021-02-25","updated":"2021-02-25","path":"post/Discoveries-7/","permalink":"https://kiko.io/post/Discoveries-7/","excerpt":"February and the first sunny days in 2021. What a delight! Have fun, sitting in the sun, discovering my newest finds on the web. This time, all regarding JavaScript… github1s: One second to read GitHub code with VS CodeHow to enhance fetch() with the Decorator PatternKy - Delightful HTTP RequestsVS Code’s REST Client Plugin is All You Need to Make API Callsjson-viewYou might not need jQueryJavaScript Algorithms and Data Structuresdate-fns - Modern JavaScript date utility libraryParsing Markdown into an Automated Table of ContentsFakeScroll - lightweight custom-looking scrollbars","keywords":"february sunny days delight fun sitting sun discovering newest finds web time javascript… github1s read github code codehow enhance fetch decorator patternky - delightful http requestsvs codes rest client plugin make api callsjson-viewyou jqueryjavascript algorithms data structuresdate-fns modern javascript date utility libraryparsing markdown automated table contentsfakescroll lightweight custom-looking scrollbars","text":"February and the first sunny days in 2021. What a delight! Have fun, sitting in the sun, discovering my newest finds on the web. This time, all regarding JavaScript… github1s: One second to read GitHub code with VS CodeHow to enhance fetch() with the Decorator PatternKy - Delightful HTTP RequestsVS Code’s REST Client Plugin is All You Need to Make API Callsjson-viewYou might not need jQueryJavaScript Algorithms and Data Structuresdate-fns - Modern JavaScript date utility libraryParsing Markdown into an Automated Table of ContentsFakeScroll - lightweight custom-looking scrollbars github1s: One second to read GitHub code with VS Code by netcon (conwnet) https://github.com/conwnet/github1s How do you peak in the code of a Github repository? Navigate back and forth on github.com? The chinese developer netcon from Shenzhen has better idea: just add the 2 characters 1s to the github url and the repository opens up in the new version of VSCode, which now can be built for browsers. Pretty handy… How to enhance fetch() with the Decorator Pattern by Dmitri Pavlutin https://dmitripavlutin.com/enhance-fetch-with-decorator-pattern/ Fetching JSON files with JavaScript means to call fetch() asynchronously and pick the response manually. Two AWAITS and a lot of stuff can go wrong. Dmitri shows how to construct a class which enables you to do this in one step. Ky - Delightful HTTP Requests by Sindre Sorhus https://github.com/sindresorhus/ky Fetch is nice, but if you want it nice and easy, you have to rely on a 3rd-party library, like *Ky. Sindre Sorhus did a great job to bring fetching in one line, within around 13KB. VS Code’s REST Client Plugin is All You Need to Make API Calls by Paige Niedringhaus https://blog.bitsrc.io/vs-codes-rest-client-plugin-is-all-you-need-to-make-api-calls-e9e95fcfd85a Using Postman or Nightingale for testing your microservices? Not absolutely necessary, as there are possibilities to do it right in VSCode, as Paige show us in her post here. No need to leave your editor. json-view by Pavel https://github.com/pgrabovets/json-view It’s not often that a developer has to display raw JSON data on a website or app. Pavel from the Ukraine has a solution to do this with style. You might not need jQuery by Zack Bloom and Adam Schwartz http://youmightnotneedjquery.com/ Many of us relied on jQuery in the past. So did Zack Bloom and Adam Schwartz as I suppose. They have published a website, that contrasts the native JavaScript methods for the most common jQuery methods. Go Vanilla, go! JavaScript Algorithms and Data Structures by Oleksii Trekhleb https://github.com/trekhleb/javascript-algorithms Oleksii has collected a huge bunch of useful JS methods in his Github repository and has translated the docs for every method into 14 (!) languages. Whoop … what a job! Ever wanted to know how to calculate the Euclidean Distance? Oleksii has the answer and the code. date-fns - Modern JavaScript date utility library by {Many} https://date-fns.org/ moment.js, maybe the most used JS library for calculating dates, is now in maintenance mode, because it is getting on in years. A good alternative is date-fns, which supports tree-shaking and other modern approaches. In addition to that, you will find here and here good comparisons between several date libraries or even native JS. Parsing Markdown into an Automated Table of Contents by Lisi Linhart https://css-tricks.com/parsing-markdown-into-an-automated-table-of-contents/ A well-structured text has headings, subheadings and paragraphs. For the web we often write our stuff in Markdown. Lisi shows us how to process such a Markdown file to get a TOC automatically. FakeScroll - lightweight custom-looking scrollbars by Yair Even Or https://github.com/yairEO/fakescroll The scrollbar belongs to the website or app a developer is creating, in my opinion. Therefore it is a mess what browser manufacturers offer developers in terms of possibilities. Yair has constructed a JS library which replaces the build-in scrollbars completely with standard HTML elements. Nice…","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Native JavaScript Multilanguage Templating","subtitle":null,"date":"2021-02-24","updated":"2021-02-24","path":"post/Native-JavaScript-Multilanguage-Templating/","permalink":"https://kiko.io/post/Native-JavaScript-Multilanguage-Templating/","excerpt":"In the project I’m currently working on, I faced the “problem” to integrate multilanguage support, but due to the fact that the new app should be written in vanilla JS, without any plugins, libraries or other dependencies, I had to develop my own localization layer. In this article I want to show you my approach on this…","keywords":"project im working faced problem integrate multilanguage support due fact app written vanilla js plugins libraries dependencies develop localization layer article show approach this…","text":"In the project I’m currently working on, I faced the “problem” to integrate multilanguage support, but due to the fact that the new app should be written in vanilla JS, without any plugins, libraries or other dependencies, I had to develop my own localization layer. In this article I want to show you my approach on this… My solution is based on a template system that I implemented into my project at an earlier stage. If you are interested in how this works, I recommend you read my article Utilize a repository of reusable ES6 template literals. Let’s start with the standard scaffold of an HTML5 app, extended with some style‘s, an initialization script and a lonely main element, we want to fill with some localized content: index.html1234567891011121314151617181920212223242526<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8" /> <meta http-equiv="language" content="en"> <title>Native JavaScript Multilanguage Templating</title> <style> body { padding: 2rem; } main { text-align: center; } </style> <script type="module"> import { App } from './app.js'; window.app = new App(); app.init(); </script> </head> <body> <main></main> </body></html> The script points to the following ES6 module class in the file app.js: app.js1234567891011class App { constructor() { // do something when the class is instantiated } init() { // do something to initialize the app }}export { App }; Nothing uncommon so far, if you are familiar with ES6 classes and imports/exports. Now let’s create a localizations.js file, to store the needed localized strings in all wanted languages. Every language will have its own branch in a Localizations object, represented by its two-letter ISO-639-1 language code. All translations are accessible via an unique english key word: localizations.js12345678910111213141516171819202122export function Localizations() { return { "EN": { "helloWorld": "Hello World" }, "DE": { "helloWorld": "Hallo Welt" }, "ES": { "helloWorld": "Hola, mundo" }, "FR": { "helloWorld": "Bonjour le monde" }, "RU": { "helloWorld": "Здравствуйте, мир" }, "JP": { "helloWorld": "ハローワールド" } }} As we import the localizations.js in our app.js, we can initialize the localizations in the constructor of the app class with the language code of the users browser: app.js12345678910111213141516import { Localizations } from './localizations.js';class App { constructor() { // Get browser language this.langCode = window.navigator.language.split("-")[0].toUpperCase(); // Init localization to access via 'app.localization' globally this.localization = Localizations()[this.langCode]; } ... app.localization now holds the key/value list of the current language. Now we implement the templating class, as described in Utilize a repository of reusable ES6 template literals and define a first template called helloWorld … templates.js123456789101112131415class Templates { helloWorld(data) { return this.fillTemplate(` <h1>${app.localization.helloWorld}</h1> `, data); } fillTemplate(templateString, templateVars){ var func = new Function(...Object.keys(templateVars), "return `" + templateString + "`;"); return func(...Object.values(templateVars)); }}export { Templates }; The inner text of the h1 element in the helloWorld template refers to the globally available variable app.localization, we initialized in the last step, and points to the translation helloWorld. In app.js we import the templates.js and implement some code in the init method, to get the template and bring it to the DOM: app.js1234567891011121314151617import { Localizations } from './localizations.js';import { Templates } from './templates.js';class App { ... init() { // Get "Hello World" H1 element in current language let helloWorld = app.templates.helloWorld({}); //Insert H1 element into MAIN element document.querySelector("main") .insertAdjacentHTML("beforeend", helloWorld); } This is it … In the following Github repository you will find a solution based on this example, extented with a language selector, cookie support and some helper methods to keep the code nice and clean: https://github.com/kristofzerbe/Native-JavaScript-Multilanguage-Templating","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Localization","slug":"Localization","permalink":"https://kiko.io/tags/Localization/"},{"name":"ES6","slug":"ES6","permalink":"https://kiko.io/tags/ES6/"},{"name":"Templating","slug":"Templating","permalink":"https://kiko.io/tags/Templating/"}]},{"title":"Remote Testing and Debugging with Chrome","subtitle":"How to test a local site on a mobile device and debug it locally","series":"Step By Step","date":"2021-01-24","updated":"2021-01-24","path":"post/Remote-Testing-and-Debugging-with-Chrome/","permalink":"https://kiko.io/post/Remote-Testing-and-Debugging-with-Chrome/","excerpt":"Developing a website or web app means, you have installed an editor locally on your computer, writing your code locally and start a tiny, built-in web server for debugging locally in your preferred browser. In most browsers, there are some features to mimic a smartphone, to see if your solution is working on such a device too, but you only get a hint if it’s running properly. Some mobile features like navigator.canShare do not work at all. Better is to see it live on your device. This article will show you firstly, how to test your local solution on a smartphone and secondly, how to debug it locally, when it runs on the smartphone after releasing.","keywords":"developing website web app means installed editor locally computer writing code start tiny built-in server debugging preferred browser browsers features mimic smartphone solution working device hint running properly mobile navigatorcanshare work live article show firstly test local debug runs releasing","text":"Developing a website or web app means, you have installed an editor locally on your computer, writing your code locally and start a tiny, built-in web server for debugging locally in your preferred browser. In most browsers, there are some features to mimic a smartphone, to see if your solution is working on such a device too, but you only get a hint if it’s running properly. Some mobile features like navigator.canShare do not work at all. Better is to see it live on your device. This article will show you firstly, how to test your local solution on a smartphone and secondly, how to debug it locally, when it runs on the smartphone after releasing. I will use following setup: Editor: Visual Studio Code Smartphone: Android Browser for Desktop & Mobile: Microsoft Edge (any other Chromium based browser will work also) Before we start, we have to enable the Android smartphone to connect to other devices, by switching on USB Debugging: Enable the Developer Options Go to Settings > About Phone Tap 7 times on Build Number Enable USB Debugging Go to Settings > System > Advanced > Developer Options Switch USB debugging to ON 1. Test your local site on a mobile deviceWhen you start your local web server from VS Code, your solution can be accessed by a localhost address at a specific port: Even if you are in the same network with all your devices, this address is only available locally. You need to “announce” this address to your mobile device by using the mechanism called Port Forwarding. This is a job for the browser… Connect you mobile device via USB with your local machine Open up chrome://inspect/#devices in your Chromium based browser (works in all Chromium browsers) Your mobile device will ask you to allow USB-Debugging … say ALLOW Under Devices, your mobile device will appear after a few seconds … my is here the Pixel 4 Click on Port Forwarding Enter your local, to be forwarded address ('localhost:' and port number) and check Enable port forwarding Open your Chromium based browser on your mobile device Enter the URL localhost:4000 Your local solution will now be loaded on your mobile device and you will see this in your local DevTools: 2. Debug a site running on your mobile device locallyThis step is now very easy, because we are connected to the mobile device and a remote site is loaded. Just click inspect at the appropriate item: . This works now also on the released version of your solution, you want to debug. Just enter the URL in a new tab on your mobile device, find the item in DevTools-Devices and click on inspect. The window, which will be opened on inspect, are the Chrome Developer Tools and every interaction with it, will be reflected on your mobile device, as you are used to when debugging locally: More Info Chrome DevTools: Access Local ServersChrome DevTools: Get Started with Remote Debugging Android Devices","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"},{"name":"Debugging","slug":"Debugging","permalink":"https://kiko.io/tags/Debugging/"}]},{"title":"Discoveries #6","subtitle":null,"series":"Discoveries","date":"2021-01-20","updated":"2021-01-20","path":"post/Discoveries-6/","permalink":"https://kiko.io/post/Discoveries-6/","excerpt":"2020 is over and history. Well, may 2021 be with us. With this post I would like to continue the Discoveries, with new momentum. There was a lot to read over the holidays. All the finds in this issue have something to do with visual aspects of web design in the broadest sense. Drop-Shadow: The Underrated CSS Filtercss.gg - 700+ CSS IconsSVGBox - API for Web IconsChange Color of SVG on HoverCSS filter generator to convert from black to target hex colorResponsive && Configurable SVG WavesBalloon.cssHow to Build a CSS-only Organizational ChartShiftyBrad Traversy's 50 Projects 50 Days","keywords":"history post continue discoveries momentum lot read holidays finds issue visual aspects web design broadest sense drop-shadow underrated css filtercssgg - 700+ iconssvgbox api iconschange color svg hovercss filter generator convert black target hex colorresponsive && configurable wavesballooncsshow build css-only organizational chartshiftybrad traversy's projects days","text":"2020 is over and history. Well, may 2021 be with us. With this post I would like to continue the Discoveries, with new momentum. There was a lot to read over the holidays. All the finds in this issue have something to do with visual aspects of web design in the broadest sense. Drop-Shadow: The Underrated CSS Filtercss.gg - 700+ CSS IconsSVGBox - API for Web IconsChange Color of SVG on HoverCSS filter generator to convert from black to target hex colorResponsive && Configurable SVG WavesBalloon.cssHow to Build a CSS-only Organizational ChartShiftyBrad Traversy's 50 Projects 50 Days Drop-Shadow: The Underrated CSS Filter by Michelle Barker https://css-irl.info/drop-shadow-the-underrated-css-filter There are 2 built-in ways to drop a shadow on a HTML element with CSS. Michelle shows us the difference and the additional features filter: drop-shadow has. css.gg - 700+ CSS Icons by Astrit Malësia https://css.gg Astrit, a swedish designer, has build this outstanding icon repository, with tons of icons in pure CSS or SVG. Chapeau… SVGBox - API for Web Icons by ? https://svgbox.net In case css.gg has not the appropriate SVG icon you are looking for, visit this site and explorer 12 icon sets with over 3.000 icons. Whoever brought this to us, thanks. Change Color of SVG on Hover by Chris Coyier https://css-tricks.com/change-color-of-svg-on-hover Once again Chris, who shows us here, how to colorize a SVG icon in case you use it as a background image by using CSS filter. CSS filter generator to convert from black to target hex color by Barrett Sonntag https://codepen.io/sosuke/pen/Pjoqqp As Chris Coyer mentioned in the discovery above, you need the appropriate filter values on colorizing SVG’s. Barret has developed a converter in a pen. Useful tool… Responsive && Configurable SVG Waves by Jhey Tompkins https://codepen.io/jh3y/pen/poEvKxo Seperating content on a web site with some kind of divider is advisable and motion is nice, if you don’t overdo it. Jhey shows us, how to combine both with an animated wave. Balloon.css by Claudio Holanda https://kazzkiq.github.io/balloon.css Ever needed tooltips for elements, which are not self-describing? Download this pure CSS solution from Claudio und you never search again for something like that. How to Build a CSS-only Organizational Chart by someone at Envato Tuts+ https://codepen.io/tutsplus/pen/MWedpoj Org charts are important to visualize hierarchies. Why not creating them with nothing else than HTML and CSS? Shifty by Warren Galyen https://wgalyen.github.io/shifty No one can escape parallax effects on backgrounds. They are just too pleasing. Warren addresses this with his tiny JavaScript library. Brad Traversy's 50 Projects 50 Days by Brad Traversy https://github.com/bradtraversy/50projects50days There are a lot of doing-a-thing-every-day projects, but Brad is pushing it with his 50 web projects in 50 days. Check out Expanding Cards or Rotating Navigation Animation or Theme Clock. Cool stuff and source code is available.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Use a duplicate image to drop a shadow","subtitle":"An alternative for 'box-shadow' on images","date":"2021-01-20","updated":"2021-01-20","path":"post/Use-a-duplicate-image-to-drop-a-shadow/","permalink":"https://kiko.io/post/Use-a-duplicate-image-to-drop-a-shadow/","excerpt":"Depending on your design, sometimes it is nice to drop a shadow on an image to highlight it: 1<img src="my-image.jpg" /> 123img { box-shadow: 0px 25px 25px -10px #666;} But … it looks like a paper print of the image, with a light bulb in the first third above it. The shade is grey, boring and has been used and seen many times before… An design related Instagram post from Muhammad Abdull of thewilsonthings, inspired me to use the image itself as the shadow in order to make the image look a bit translucent. Should be the same technique as that of a reflection. Here is the HTML/CSS code for it, as the people asking for it in the comments.","keywords":"depending design nice drop shadow image highlight 1<img src="my-imagejpg" /> 123img { box-shadow 0px 25px -10px #666} … paper print light bulb shade grey boring times before… related instagram post muhammad abdull thewilsonthings inspired order make bit translucent technique reflection html˼ss code people comments","text":"Depending on your design, sometimes it is nice to drop a shadow on an image to highlight it: 1<img src="my-image.jpg" /> 123img { box-shadow: 0px 25px 25px -10px #666;} But … it looks like a paper print of the image, with a light bulb in the first third above it. The shade is grey, boring and has been used and seen many times before… An design related Instagram post from Muhammad Abdull of thewilsonthings, inspired me to use the image itself as the shadow in order to make the image look a bit translucent. Should be the same technique as that of a reflection. Here is the HTML/CSS code for it, as the people asking for it in the comments. What we want to achieve is this: Basically, we use a duplicate of the image and position it below the actual image, but slightly offset and blurred. It won’t be a performance issue, as some might think, because it is the very same file and will be loaded only once by the browser. For showing two images in the nearly same place in different layers, we need a wrapper… 1234<div class="image-wrapper"> <img src="my-image.jpg" /> <img class="shadow" src="my-image.jpg" /></div> … and some CSS for positioning the images on top of each other first. Here are the defaults for both image elements: 123456789101112div.image-wrapper { position: relative;}div.image-wrapper img { position: absolute; display: block; top: 0; left: 0; width: 100%; z-index: 1;} Now we have to style the duplicate image that it looks similar to the shadow. We washing it out using the blur filter and the opacity. 1234div.image-wrapper img.shadow { filter: blur(10px); opacity: 0.8;} Last step is to change the duplicates dimensions and the positioning below the original image. We squeeze it by 10% and shift it from left with half of the value back to the center, shift it from top to make it standout at the bottom and send it to the back by taking a lower z-index than the original image. 123456789div.image-wrapper img.shadow { filter: blur(10px); opacity: 0.8; width: 90%; left: 5%; top: 40px; z-index: 0;} Thats it. Here’s a pen to play around with the solution:","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"}]},{"title":"Safely remove multiple classes using a prefix","subtitle":"Avoiding pitfalls when iterating over classList","date":"2021-01-18","updated":"2021-01-18","path":"post/Safely-remove-multiple-classes-using-a-prefix/","permalink":"https://kiko.io/post/Safely-remove-multiple-classes-using-a-prefix/","excerpt":"Writing a Web App with HTML and JavaScript means you deal with several classes on your DOM elements in order to visualize state changes. And there are some pitfalls to be aware of with regard to removal. Assuming you want to open some kind of sidebar above a container. In this sidebar you have several buttons to show different content via JavaScript and a close button, which closes the sidebar again. You HTML code maybe looks like this: 123456789101112131415161718<html> <body> <div id="container">... Main Content ...</div> <nav> <button id="open-sidebar"> </nav> <aside id="sidebar"> <div class="content">... Sidebar Content ...</div> <button id="close">Close Sidebar</button> <button id="content1">Show Content 1</button> <button id="content2">Show Content 2</button> <button id="content3">Show Content 3</button> </aside> </body></html> By clicking on the open-sidebar button, the sidebar is opened and the action, respectively the new state, is vizualized by adding an appropriate class to the parent sidebar element. In order to make it easy for the user, the default content (Content 1) will be loaded also and its state will be marked with another class. 1<aside class="sidebar open open-content1"> A click on of the other content buttons (let’s say Content 2), will replace the current content and the aside classes will change into: 1<aside class="sidebar open open-content2"> Now we want to close the sidebar again, assuming that we don’t have stored the currently opened content in the JavaScript code…","keywords":"writing web app html javascript means deal classes dom elements order visualize state pitfalls aware regard removal assuming open kind sidebar container buttons show content close button closes code 123456789101112131415161718<html> <body> <div id="container"> main </div> <nav> <button id="open-sidebar"> </nav> <aside id="sidebar"> class="content"> id="close">close sidebar</button> id="content1">show 1</button> id="content2">show 2</button> id="content3">show 3</button> </aside> </body></html> clicking open-sidebar opened action vizualized adding class parent element make easy user default loaded marked 1<aside class="sidebar open-content1"> click lets replace current change open-content2"> dont stored code…","text":"Writing a Web App with HTML and JavaScript means you deal with several classes on your DOM elements in order to visualize state changes. And there are some pitfalls to be aware of with regard to removal. Assuming you want to open some kind of sidebar above a container. In this sidebar you have several buttons to show different content via JavaScript and a close button, which closes the sidebar again. You HTML code maybe looks like this: 123456789101112131415161718<html> <body> <div id="container">... Main Content ...</div> <nav> <button id="open-sidebar"> </nav> <aside id="sidebar"> <div class="content">... Sidebar Content ...</div> <button id="close">Close Sidebar</button> <button id="content1">Show Content 1</button> <button id="content2">Show Content 2</button> <button id="content3">Show Content 3</button> </aside> </body></html> By clicking on the open-sidebar button, the sidebar is opened and the action, respectively the new state, is vizualized by adding an appropriate class to the parent sidebar element. In order to make it easy for the user, the default content (Content 1) will be loaded also and its state will be marked with another class. 1<aside class="sidebar open open-content1"> A click on of the other content buttons (let’s say Content 2), will replace the current content and the aside classes will change into: 1<aside class="sidebar open open-content2"> Now we want to close the sidebar again, assuming that we don’t have stored the currently opened content in the JavaScript code… What we have to do, is to iterate over all classes of aside and remove those which starts with open: 12345678910111213141516let sidebar = document.getElementById("sidebar");for (let i = 0; i < sidebar.classList.length; i++) { let value = sidebar.classList[i]; if (value.startsWith("open")) { sidebar.classList.remove(value); }}//orlet sidebar = document.getElementById("sidebar");sidebar.classList.forEach(function(value){ if(value.includes("open")) { sidebar.classList.remove(value); };}); Both approaches have a pitfall: when the first class, starting with open, is removed from the list, the length of the classList array changes immediatly and we won’t reach the last class in the list … ! The solution is to find and remove all appropriate classes at once, for example by using RegEx and a reusable helper function: 12345678910function removeClassByPrefix(el, prefix) { let pattern = '(' + prefix + '(\\\\s|(-)?(\\\\w*)(\\\\s)?)).*?'; var regEx = new RegExp(pattern, 'g'); el.className = el.className.replace(regEx, '');}//...let sidebar = document.getElementById("sidebar");removeClassByPrefix(sidebar, "open"); Update, 24 Jan 2021The first posted RegEx pattern didn’t worked properly, because it has found the prefix only and not the whole word, so I have updated the pattern. You can try it out at RegExr.com - Remove Class By Prefix.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"DOM","slug":"DOM","permalink":"https://kiko.io/tags/DOM/"}]},{"title":"Use and manage multiple Node.js versions on Windows 10","subtitle":null,"date":"2021-01-08","updated":"2021-01-08","path":"post/Use-and-manage-multiple-Node-js-versions-on-Windows-10/","permalink":"https://kiko.io/post/Use-and-manage-multiple-Node-js-versions-on-Windows-10/","excerpt":"For a new project I needed to have Node 14 running on my Windows 10 machine, so installation was done quickly via downloading and running the setup file. A short time later I wanted to write a new blog post here on kiko.io, which depends on the Node.js based static site generator Hexo … and ran into several problems. First of all my hero image processing script (see Automatic Header Images in Hexo) returned an exception. The script uses hexo-fs and the problem is known quite some time, according to this Github issue. The guys recommend to downgrade to an older version of Node.js … :( Ok … I needed a solution to install multiple Node.js versions and switch between them, depending on which project I want to work on … and there is one: nvm-windows by Corey Butler!","keywords":"project needed node running windows machine installation quickly downloading setup file short time wanted write blog post kikoio depends nodejs based static site generator hexo … ran problems hero image processing script automatic header images returned exception hexo-fs problem github issue guys recommend downgrade older version solution install multiple versions switch depending work nvm-windows corey butler","text":"For a new project I needed to have Node 14 running on my Windows 10 machine, so installation was done quickly via downloading and running the setup file. A short time later I wanted to write a new blog post here on kiko.io, which depends on the Node.js based static site generator Hexo … and ran into several problems. First of all my hero image processing script (see Automatic Header Images in Hexo) returned an exception. The script uses hexo-fs and the problem is known quite some time, according to this Github issue. The guys recommend to downgrade to an older version of Node.js … :( Ok … I needed a solution to install multiple Node.js versions and switch between them, depending on which project I want to work on … and there is one: nvm-windows by Corey Butler! This Node.js Version Manager for Windows is working similar to the often mentioned n and nvm, which support Linux and Mac only. The latest release of nvm-windows can be downloaded here. The setup is pretty straight forward and asks you at the very end, if the currently installed Node.js version should be managed by it. Confirmed… There are just a few commands to know and to run in the command line: List available Node.js versions 1nvm list available Install needed Node.js version 1nvm install <version> Switch to particular Node.js version 1nvm use <version> List all installed Node.js versions 1nvm ls In case you have Node.js version dependend utilities installed globally, you need to run npm install -g after switch. More Info Github: coreybutler/nvm-windowsMicrosoft Docs: Set up your Node.js development environment directly on Windows","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Node.js","slug":"Node-js","permalink":"https://kiko.io/tags/Node-js/"}]},{"title":"How to prevent duplicate events","subtitle":null,"date":"2021-01-07","updated":"2021-01-07","path":"post/How-to-prevent-duplicate-events/","permalink":"https://kiko.io/post/How-to-prevent-duplicate-events/","excerpt":"I’m working on a new web app that contains a sliding out panel with some additional information on the selected element. This panel can be closed by the user via the ESC key. The implementation on initializing the panel seems very straight forward: 1234567891011class Panel() { init() { document.addEventListener("keydown", function(event) { if(event.key === "Escape"){ //close the panel } }); }} Problem is: the panel, which is part of the basic HTML, will be initialized with its content and functionality in a ES6 class. So … on every init, another event listener is added. You can easily figure that out, by calling getEventListeners(document) in the Chrome DevTools:","keywords":"im working web app sliding panel additional information selected element closed user esc key implementation initializing straight forward 1234567891011class { init documentaddeventlistener"keydown" functionevent ifeventkey === "escape"{ //close } }} problem part basic html initialized content functionality es6 class … event listener added easily figure calling geteventlistenersdocument chrome devtools","text":"I’m working on a new web app that contains a sliding out panel with some additional information on the selected element. This panel can be closed by the user via the ESC key. The implementation on initializing the panel seems very straight forward: 1234567891011class Panel() { init() { document.addEventListener("keydown", function(event) { if(event.key === "Escape"){ //close the panel } }); }} Problem is: the panel, which is part of the basic HTML, will be initialized with its content and functionality in a ES6 class. So … on every init, another event listener is added. You can easily figure that out, by calling getEventListeners(document) in the Chrome DevTools: As there is no way in JS to find and replace the event which was previously added, we have to remove the existing event by using removeEventListener and add it again. Most important parameter on removing is the instance of the event handler, which was used the add the event previously. 12345678910111213class Panel() { init() { function onEscapeKey (event) { if(event.key === "Escape"){ //close the panel } } document.removeEventListener("keydown", onEscapeKey); document.addEventListener("keydown", onEscapeKey); }} But … as the handler onEscapeKey is defined in a class, every time a new instance of the class is created, the handler will be not the same as the previous one! We have to store the event handler globally… 12345678910111213class Panel() { init() { document.removeEventListener("keydown", window.panelEscapeKeyHandler); window.panelEscapeKeyHandler = function onEscapeKey(event) { if(event.key === "Escape"){ //close the panel } }; document.addEventListener("keydown", window.panelEscapeKeyHandler); }} Works!","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Events","slug":"Events","permalink":"https://kiko.io/tags/Events/"}]},{"title":"Utilize a repository of reusable ES6 template literals","subtitle":null,"date":"2021-01-03","updated":"2021-01-03","path":"post/Utilize-a-repository-of-reusable-ES6-template-literals/","permalink":"https://kiko.io/post/Utilize-a-repository-of-reusable-ES6-template-literals/","excerpt":"The Template Literals introduced with ES6 are very useful to deal with multiline strings, because they support embedded expressions. Gone are the days of endless string concatination or replacing variables in a string by using RegEx. Instead of… 12345678var url = ...var file = ...var template = '<div class="photo">' + '<a href="' + url + "' + 'style="background-image: url(' + file + ')"</a>' + '</div>' … you can write: 123456789var url = ...var file = ...var template = ` <div class="photo"> <a href="${url}/" style="background-image: url(${file});"></a> </div>`, It’s much cleaner and easier to handle, as you can copy your needed HTML right into your code and surround it by backtick (!) characters. Insert your variable placeholders (expressions), indicated by a dollar sign and curly braces, and you are done. But there is one “restriction”, you have to be aware of: the interpolation (substitution of the expressions) is done at declaration time and not at runtime. You can’t define your literals seperatly, take one and make your substitution as you need it, like you would do with Handlebars or other templating engines. Therefore the name template literals is a bit misleading. But … there is a way to achieve this anyway…","keywords":"template literals introduced es6 deal multiline strings support embedded expressions days endless string concatination replacing variables regex of… 12345678var url = var file '<div class="photo">' + '<a href="' "' 'style="background-image url' '"</a>' '</div>' … write 123456789var ` <div class="photo"> <a href="${url}/" style="background-image url${file}"></a> </div>` cleaner easier handle copy needed html code surround backtick characters insert variable placeholders dollar sign curly braces restriction aware interpolation substitution declaration time runtime define seperatly make handlebars templating engines bit misleading achieve anyway…","text":"The Template Literals introduced with ES6 are very useful to deal with multiline strings, because they support embedded expressions. Gone are the days of endless string concatination or replacing variables in a string by using RegEx. Instead of… 12345678var url = ...var file = ...var template = '<div class="photo">' + '<a href="' + url + "' + 'style="background-image: url(' + file + ')"</a>' + '</div>' … you can write: 123456789var url = ...var file = ...var template = ` <div class="photo"> <a href="${url}/" style="background-image: url(${file});"></a> </div>`, It’s much cleaner and easier to handle, as you can copy your needed HTML right into your code and surround it by backtick (!) characters. Insert your variable placeholders (expressions), indicated by a dollar sign and curly braces, and you are done. But there is one “restriction”, you have to be aware of: the interpolation (substitution of the expressions) is done at declaration time and not at runtime. You can’t define your literals seperatly, take one and make your substitution as you need it, like you would do with Handlebars or other templating engines. Therefore the name template literals is a bit misleading. But … there is a way to achieve this anyway… Tagged TemplatesBeside Template Literals, ES6 introduced Tagged Templates (exact: Tagged Template Literals). These tags are functions, which allows you to parse a Template Literal. Definition is like this: 123function myTag(literals, ...expressions) { //do the substitution and return a string} You can use these tags by prefixing you literal: 1myTag`Hello ${firstName} ${lastName}!` Using Tagged Templates to build a template repository would mean, you have to write one tag function for every template … doable, but time consuming. Dynamic Tag FunctionTo avoid this, we can write a universal tag function, which utilizes the Function constructor, to create the tag function dynamically: 12345678function fillTemplate(templateString, templateVars) { var func = new Function( ...Object.keys(templateVars), "return `" + templateString + "`;") return func(...Object.values(templateVars));} Don’t use this approach on user inputs as expressions, to avoid XSS! Let’s see an example…Given is a tiny web app with the following structure: index.html123456789101112<!DOCTYPE html><html> <head> <title>Reusable ES6 template literals</title> <meta charset="UTF-8" /> <link rel="stylesheet" href="/src/style.css"> </head> <body> <main id="main"></main> <script src="src/index.js"></script> </body></html> index.js12345import { App } from "./app.js";const app = new App();app.init(); app.js123456class App { init() { //do something }}export { App }; What we want to do now, is to load some images into the main element, by using a more or less complex element structure: 1234<div class="photo"> <a href="<!-- Url to view the photo -->" style="background-image: url(<!-- Url of the photo file -->);"></a></div> To separate our templates from the main code, we create a template module, which contains the dynamic tag function from above and a photo template we want to use in our app template.js12345678910111213141516171819202122232425class Templates { //Template photo(data) { return this.fillTemplate( ` <div class="photo"> <a href="${data.url}/" style="background-image: url(${data.file});"></a> </div> `, data ); } //Dynamic Tag Function fillTemplate(templateString, templateVars) { var func = new Function(...Object.keys(templateVars), "return `" + templateString + "`;" ); return func(...Object.values(templateVars)); } }export { Templates }; The template retrieves a data object, with the values of the defined expressions, and calls the dynamic tag function on the literal template. This we can use now in our app code: app.js12345678910111213141516171819202122//Import Template moduleimport { Templates } from "./templates.js";class App { init() { //Initialize Templates this._templates = new Templates(); //Insert photo into MAIN element let main = document.getElementById("main"); main.insertAdjacentHTML( "beforeend", this._templates.photo({ file: "my-photo.jpg", url: "https://link-to-my.photo.com" }) ); }}export { App }; See it live at codesandbox.io. More Info Stackoverflow: Can ES6 template literals be substituted at runtime (or reused)?Github/Adelphos: ES6-Reuseable-Template","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"ES6","slug":"ES6","permalink":"https://kiko.io/tags/ES6/"},{"name":"Templating","slug":"Templating","permalink":"https://kiko.io/tags/Templating/"}]},{"title":"Spice Up Windows Terminal","subtitle":"How to make Powershell a little prettier","date":"2020-12-24","updated":"2021-04-16","path":"post/Spice-Up-Windows-Terminal/","permalink":"https://kiko.io/post/Spice-Up-Windows-Terminal/","excerpt":"Working with the PowerShell in 2020 means fun, because of the new Windows Terminal (get it from Windows Store). It has more power as the old Powershell Console and it is visually adaptable to your personal taste, by installing the wonderful theming engine oh-my-posh from Jan De Dobbeleer. To get Oh-My-Posh properly run, there are several steps to do I want to show here in a nutshell…","keywords":"working powershell means fun windows terminal store power console visually adaptable personal taste installing wonderful theming engine oh-my-posh jan de dobbeleer properly run steps show nutshell…","text":"Working with the PowerShell in 2020 means fun, because of the new Windows Terminal (get it from Windows Store). It has more power as the old Powershell Console and it is visually adaptable to your personal taste, by installing the wonderful theming engine oh-my-posh from Jan De Dobbeleer. To get Oh-My-Posh properly run, there are several steps to do I want to show here in a nutshell… Step 1 - Install a suitable fontAs the theming engine uses Powerline glyphs, you need to install a font which support them, for example the new Cascadia Code PL from Microsoft. Download, unzip and install the OTF and/or TTF font files via context menu in your Windows Explorer. Step 2 - Set new font in your settingsOpen up you Terminal settings… … and add following new line to the PowerShell section: 1"fontFace": "Cascadia Code PL", Step 3 - Install oh-my-poshFollowing PowerShell command installs the theming engine itself: 1Install-Module oh-my-posh -Scope CurrentUser If you want to display Git status information also, run this command: 1Install-Module posh-git -Scope CurrentUser To let the command-line editing environment to be customized install PSReadline: 1Install-Module -Name PSReadLine -Scope CurrentUser -Force -SkipPublisherCheck Step 4 - Load on startupIn order to load the theming engine in every new terminal window, edit your PowerShell profile by opening it up with the command … 1notepad $PROFILE and add following lines in the upcoming text file: 123Import-Module oh-my-poshImport-Module posh-gitSet-Theme Paradox Paradox is one of 27 themes available. You will find all themes in your DOCUMENTS folder under ..\\WindowsPowerShell\\Modules\\oh-my-posh\\<version>\\Themes and some visual representations at https://github.com/JanDeDobbeleer/oh-my-posh?#themes. #Update, April 2021Scott Hanselman has mentioned a new improvement recently: Show suitable icons on listing files: Download and install CaskaydiaCove Nerd Font at https://www.nerdfonts.com/font-downloads Open Terminal Settings (like in Step 2) Replace the fontface with "CaskaydiaCove Nerd Font" Run Install-Module -Name Terminal-Icons -Repository PSGallery in Terminal, opened as administrator Add Import-Module -Name Terminal-Icons in your profile (like in Step 4) You will get this on calling dir, for example: More Info Windows Store: Windows TerminalMicrosoft: Cascadia Code PLGitHub: JanDeDobbeleer/oh-my-poshMicrosoft Docs: Set up Powerline in Windows TerminalScott Hanselman: How to make a pretty prompt in Windows Terminal with Powerline, Nerd Fonts, Cascadia Code, WSL, and oh-my-poshScott Hanselman: Taking your PowerShell prompt to the next level with Windows Terminal and Oh my Posh 3Scott Hanselman: Take your Windows Terminal and PowerShell to the next level with Terminal Icons","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Theming","slug":"Theming","permalink":"https://kiko.io/tags/Theming/"},{"name":"PowerShell","slug":"PowerShell","permalink":"https://kiko.io/tags/PowerShell/"}]},{"title":"Discoveries #5","subtitle":null,"series":"Discoveries","date":"2020-12-19","updated":"2020-12-19","path":"post/Discoveries-5/","permalink":"https://kiko.io/post/Discoveries-5/","excerpt":"In this episode of the Discoveries (almost) everything is about images and the web. There are so many pitfalls to do it wrong, but many more possibilities to do it right, especially with these resources I found in the last few weeks. ASP.NET Core Image Resizing MiddlewareBest way to lazy load images for maximum performanceimage orientation on the webcosha - Colorful shadows for your imagesparax-bg - Parallax Backgroundsparax - Parallax ElementsLuminous LightboxTiny-Swiper - Image Carousel","keywords":"episode discoveries images web pitfalls wrong possibilities resources found weeks aspnet core image resizing middlewarebest lazy load maximum performanceimage orientation webcosha - colorful shadows imagesparax-bg parallax backgroundsparax elementsluminous lightboxtiny-swiper carousel","text":"In this episode of the Discoveries (almost) everything is about images and the web. There are so many pitfalls to do it wrong, but many more possibilities to do it right, especially with these resources I found in the last few weeks. ASP.NET Core Image Resizing MiddlewareBest way to lazy load images for maximum performanceimage orientation on the webcosha - Colorful shadows for your imagesparax-bg - Parallax Backgroundsparax - Parallax ElementsLuminous LightboxTiny-Swiper - Image Carousel ASP.NET Core Image Resizing Middleware by Jeremy Paddison https://www.paddo.org/asp-net-core-image-resizing-middleware/ Jeremy shows in his blog post the possibilities of dealing with images in ASP.NET Core in terms of format, orientation and quality. A must read for every .NET developer. Best way to lazy load images for maximum performance by Adrian Bece https://blog.prototyp.digital/best-way-to-lazy-load-images-for-maximum-performance/ Delivering images on the web is difficult due to different devices and bandwidths. Adrian shows how to achieve a maximum of performance on lazy loading images via native JavaScript. image orientation on the web by Michael Scharnagl https://justmarkup.com/articles/2019-10-21-image-orientation/ Automatic uploading and viewing images fails sometimes on portrait shots, because of misintrepretated orientation information by the different browsers. Michael adresses this problem with a Node.JS solution. cosha - Colorful shadows for your images by Robin Löffel https://github.com/robinloeffel/cosha Adding a blurry shadow under an image to let the photo stand out from the background, is a nice technique to draw the users attention. Robin goes one step further with his JavaScript solution on colorful shadows, which represents the colors of the image. parax-bg - Parallax Backgrounds by Tobias Buschor https://github.com/nuxodin/parax-bg Parallaxing backgrounds are fairly attracting and so it is with Tobias’ approach on that for developers: easy to use and fast. parax - Parallax Elements by Tobias Buschor https://github.com/nuxodin/parax If you just want to parallax some elements instead of backgrounds, Tobias has also a solution for that: Parax. Luminous Lightbox by imgix https://github.com/imgix/luminous There are tons of image lightboxes out there and here is another one: Luminous from imgx. It is my favourite and I use it in this blog. Its lightweight and easy to use, for the user and the developer. A pearl… Tiny-Swiper - Image Carousel by Joe Harris https://tiny-swiper.joe223.com/docs/demo/ The same applies to image carousels, but Tiny-Swiper is here outstanding too. It is simple, but powerful and well documented. A must use…","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Indian Presets for Lightroom","subtitle":null,"series":"Lightroom Presets","date":"2020-10-28","updated":"2020-10-28","path":"post/Indian-Presets-for-Lightroom/","permalink":"https://kiko.io/post/Indian-Presets-for-Lightroom/","excerpt":"In 2019 I was on a short, stressful business trip to Dehli, India and one night we had the opportunity to relax a bit by driving around the city and visit some beautiful places of interest. I had no gear at all, just my Sony smartphone, but it is remarkable how good this worked out.","keywords":"short stressful business trip dehli india night opportunity relax bit driving city visit beautiful places interest gear sony smartphone remarkable good worked","text":"In 2019 I was on a short, stressful business trip to Dehli, India and one night we had the opportunity to relax a bit by driving around the city and visit some beautiful places of interest. I had no gear at all, just my Sony smartphone, but it is remarkable how good this worked out. Indian Sunset var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-w1bsa5\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-ckszih\"), { controlColor: themeColor, controlShadow: false, verticalMode: true, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Indian Sunset.xmp","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Presets","slug":"Presets","permalink":"https://kiko.io/tags/Presets/"}]},{"title":"Israeli Presets for Lightroom","subtitle":null,"series":"Lightroom Presets","date":"2020-10-27","updated":"2020-10-27","path":"post/Israeli-Presets-for-Lightroom/","permalink":"https://kiko.io/post/Israeli-Presets-for-Lightroom/","excerpt":"I’m a travel and event photo enthusiast, which means I’m shooting a lot of photographs on vacation or at special events only a few times a year. After I’m back home and start the image processing, I develop a particular look for my images of the past vacation or event. This has a lot to do with my mood and is very intuitive. Not all images are the same in terms of composition and light and so I create usually 3 or 4 different presets each time during image processing. Back in 2019, I was traveling around Israel, a fascinating country where almost every wall has a story to tell and I was listening through my viewfinder. Here I want to share the presets with you…","keywords":"im travel event photo enthusiast means shooting lot photographs vacation special events times year back home start image processing develop images past mood intuitive terms composition light create presets time traveling israel fascinating country wall story listening viewfinder share you…","text":"I’m a travel and event photo enthusiast, which means I’m shooting a lot of photographs on vacation or at special events only a few times a year. After I’m back home and start the image processing, I develop a particular look for my images of the past vacation or event. This has a lot to do with my mood and is very intuitive. Not all images are the same in terms of composition and light and so I create usually 3 or 4 different presets each time during image processing. Back in 2019, I was traveling around Israel, a fascinating country where almost every wall has a story to tell and I was listening through my viewfinder. Here I want to share the presets with you… Israeli ColorsThe mediaval walls of Jaffa glow in an inimitable way and brings other colors to shine the same way. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-woscep\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Israeli Colors.xmp Israeli LightsThe light in the eastern Mediterranean is stunning. The warm tone of the sand and the turquoise color of the water had to pop out. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-2uuaut\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Israeli Lights.xmp Israeli DramaA visit of Yad Vashem moved me a lot and this preset is a expression of that. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-tibto6\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Israeli Drama.xmp Israeli Near BlackIf you think of the tourists away, Jerusalem takes you to another level because of its age and history and nothing fits more to that than the sepia look of old pictures. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-820ubz\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Israeli Near Black.xmp","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Presets","slug":"Presets","permalink":"https://kiko.io/tags/Presets/"}]},{"title":"Folder based publishing in Lightroom","subtitle":null,"date":"2020-10-26","updated":"2020-10-26","path":"post/Folder-based-publishing-in-Lightroom/","permalink":"https://kiko.io/post/Folder-based-publishing-in-Lightroom/","excerpt":"In all times photography was a process: First you shoot you images, then you edit them and in the third step you publish them elsewhere. Today Adobe Lightroom is a de-facto standard in photo processing, especially when you shoot RAW images. And I mean Lightroom Classic and not the new web/smartphone based software, which doesn’t come even close to the desktop application yet. I use Lightroom for all purposes after I shot my images: editing, cataloguing, managing and … publishing. On publishing Lightroom offers you two approaches: Export and Publish. While Export is mainly for creating JPG copies of edited RAW images, Publish goes one step further and gives you the ability to do “something” with your exported JPG’s, for example upload them on Flickr, Instagram, 500px or to your own web server via FTP. Major difference to Export is, that Publish keeps your images in sync. Everytime you change the source images, the defined publish services recognizes and offer you to re-publish your image. In the past years I tried a lot of Lightroom plugins for publishing on several platforms, but it doesn’t work out for long, because all these platforms change their API almost every year (or are stamped) and the sparely maintained 3rd party plugins break. My workflow for quite some time is to publish my photos on the hard drive, in a folder, which is synced via Dropbox with the cloud. From there I distribute them further.","keywords":"times photography process shoot images edit step publish today adobe lightroom de-facto standard photo processing raw classic web/smartphone based software doesnt close desktop application purposes shot editing cataloguing managing … publishing offers approaches export creating jpg copies edited ability exported jpgs upload flickr instagram 500px web server ftp major difference sync everytime change source defined services recognizes offer re-publish image past years lot plugins platforms work long api year stamped sparely maintained 3rd party break workflow time photos hard drive folder synced dropbox cloud distribute","text":"In all times photography was a process: First you shoot you images, then you edit them and in the third step you publish them elsewhere. Today Adobe Lightroom is a de-facto standard in photo processing, especially when you shoot RAW images. And I mean Lightroom Classic and not the new web/smartphone based software, which doesn’t come even close to the desktop application yet. I use Lightroom for all purposes after I shot my images: editing, cataloguing, managing and … publishing. On publishing Lightroom offers you two approaches: Export and Publish. While Export is mainly for creating JPG copies of edited RAW images, Publish goes one step further and gives you the ability to do “something” with your exported JPG’s, for example upload them on Flickr, Instagram, 500px or to your own web server via FTP. Major difference to Export is, that Publish keeps your images in sync. Everytime you change the source images, the defined publish services recognizes and offer you to re-publish your image. In the past years I tried a lot of Lightroom plugins for publishing on several platforms, but it doesn’t work out for long, because all these platforms change their API almost every year (or are stamped) and the sparely maintained 3rd party plugins break. My workflow for quite some time is to publish my photos on the hard drive, in a folder, which is synced via Dropbox with the cloud. From there I distribute them further. Important on this approach is, to have a coherent output folder structure in order find a particular image afterwards. I store my RAW images on an external hard drive in a structure like this: 1234<Drive:\\> -> Fotos -> <Year> -> <Year>-<Month> <Eventname> First step after shooting is to copy all RAW files from the SD Card into a new subfolder of the current year. From there I import them into my Lightroom catalog. After sorting, rejecting, editing and flagging in Lightroom, I have to “export” the 4- and 5-star rated images into the cloud. There I have a slightly different structure, with a different root folder name and without the year: 123<Dropbox> -> Photos -> <Year>-<Month> <Eventname> With the built-in “Hard Drive” publish service of Lightroom, it is feasible to “export” the images, but not in my wanted folder structure, because it is not possible to use the sources’ folder name as output folder name by option. It has to be specified manually each time, which is not very comfortable. Plugin to the rescueJeffrey Friedl, who is in the Lightroom plugin business about a decade, offers two plugins, which can solve the problem easily: Jeffrey’s “Folder Publisher“ Lightroom Plugin Exports to disk in a folder hierarchy that mimics the folder hierarchy in your Lightroom catalog This one helps me not much, because my output folder would look like this: 12345<Dropbox> -> Photos -> Fotos -> <Year> -> <Year>-<Month> <Eventname> Jeffrey’s “Collection Publisher” Lightroom PluginExports to local disk in a folder hierarchy that mimics the collection hierarchy you build within Lightroom Besides the commonality of defining a root folder, where the files are exported to, Jeffrey’s plugin has much more options to improve the export. As the main feature, you can create collections within the plugin to define the export targets: In the options of these collections, you can set several templates (variables) to let the plugin name the export subfolder automatically: By using Smart Collections you don’t even need to drag & drop your images to publish to the publish collection. Lightroom will do this auto-magically.","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"}]},{"title":"Discoveries #4","subtitle":null,"series":"Discoveries","date":"2020-10-10","updated":"2020-10-10","path":"post/Discoveries-4/","permalink":"https://kiko.io/post/Discoveries-4/","excerpt":"It is so amazing how many cool stuff developers around the world are producing these days. Or they do what they always do, but I have more time to read about their smart ideas and solutions. This month I have 8 pearls for you: waitForElementTransition()Using Flexbox and text ellipsis togetherUsing Trello as a Super Simple CMSMemorize Scroll Position Across Page LoadsA free guide to HTML5 <head> elementsBVSelect - Vanilla JSA clock that represents the time as hex color valuesAnimate.css - Just-add-water CSS animations","keywords":"amazing cool stuff developers world producing days time read smart ideas solutions month pearls waitforelementtransitionusing flexbox text ellipsis togetherusing trello super simple cmsmemorize scroll position page loadsa free guide html5 <head> elementsbvselect - vanilla jsa clock represents hex color valuesanimatecss just-add-water css animations","text":"It is so amazing how many cool stuff developers around the world are producing these days. Or they do what they always do, but I have more time to read about their smart ideas and solutions. This month I have 8 pearls for you: waitForElementTransition()Using Flexbox and text ellipsis togetherUsing Trello as a Super Simple CMSMemorize Scroll Position Across Page LoadsA free guide to HTML5 <head> elementsBVSelect - Vanilla JSA clock that represents the time as hex color valuesAnimate.css - Just-add-water CSS animations waitForElementTransition() by Mark Kennedy https://github.com/mkay581/wait-for-element-transition In these days a good UI doesn’t do without some animations or transitions and it is always advisible to use CSS for it, if possible. Marks shows us with his solution, how to wait in JavaScript for a transition to finish, before we continue to do something else in JS. Using Flexbox and text ellipsis together by Leonardo Faria https://leonardofaria.net/2020/07/18/using-flexbox-and-text-ellipsis-together/ In case you offer downloads with very long file names from time to time, you might use CSS’s ellipsis to cut it down. But you always loose the last three chars, the file extension. Leonardo show us, how to avoid that, by using a clever mix of ellipsis and flexbox. Using Trello as a Super Simple CMS by Phil Hawksworth https://css-tricks.com/using-trello-as-a-super-simple-cms As I love Trello and use it daily, among others as a reading list (see Add website to Trello card the better way), I can’t wait to try Phils approach to process Trello boards automatically. Memorize Scroll Position Across Page Loads by Chris Coyier https://css-tricks.com/memorize-scroll-position-across-page-loads/ Chris shows Hakim El Hattab’s trick, how to store the current scroll position and restore it when user comes back. Simple, but a gain in usability. A free guide to HTML5 <head> elements by Josh Buchea https://htmlhead.dev Doing your Web Developer job right, means you have to be aware of the META tags in your HTML. htmlhead.dev is a good reference, because it lists and describes mostly all known META tags. BVSelect - Vanilla JS by Bruno Vieira https://bmsvieira.github.io/BVSelect-VanillaJS There are many HTML/Javascript driven dropdowns out there. So has Bruno, but his solution don’t even looks nice, it is written in ES6, has no dependencies and is dead simple to use. A clock that represents the time as hex color values by Jamel Hammoud https://github.com/JamelHammoud/hextime The time is shown mostly as a six digit number … Hours, Minutes and Seconds, with a leading 0. Color Hex codes have also 6 digits and Jamel the idea to bring both together… Animate.css - Just-add-water CSS animations by Daniel Eden https://animate.style Daniel and his buddies offers an Stylesheet with dozens of cool and easy to use text animations.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Implement source switch for SPA","subtitle":"Asynchronous loading of JS and CSS depending on the environment","date":"2020-10-04","updated":"2020-10-04","path":"post/Implement-source-switch-for-SPA/","permalink":"https://kiko.io/post/Implement-source-switch-for-SPA/","excerpt":"A while ago I wrote a Single Page Application (SPA) with jQuery and and decided to use some useful plugins to avoid reinventing the wheel. To keep the delivered sources small, I used the bundler Gulp, to pack all JS plugins in a single file and another one for my custom JS code. I used the same procedure with the CSS files. The SPA contained only a single HTML file in which all bundeled sources and needed HTML template blocks were included, in order to load most of the stuff while starting the app, when the users sees a GMail-like loading screen. But the whole thing had one disadvantage: Debugging for example in Chrome Dev Tool is not a joy, if the code is packed with Gulp Concat and Gulp Uglify. It would be much more convenient, if the source loading can be done depending on the environment.","keywords":"ago wrote single page application spa jquery decided plugins avoid reinventing wheel delivered sources small bundler gulp pack js file custom code procedure css files contained html bundeled needed template blocks included order load stuff starting app users sees gmail-like loading screen thing disadvantage debugging chrome dev tool joy packed concat uglify convenient source depending environment","text":"A while ago I wrote a Single Page Application (SPA) with jQuery and and decided to use some useful plugins to avoid reinventing the wheel. To keep the delivered sources small, I used the bundler Gulp, to pack all JS plugins in a single file and another one for my custom JS code. I used the same procedure with the CSS files. The SPA contained only a single HTML file in which all bundeled sources and needed HTML template blocks were included, in order to load most of the stuff while starting the app, when the users sees a GMail-like loading screen. But the whole thing had one disadvantage: Debugging for example in Chrome Dev Tool is not a joy, if the code is packed with Gulp Concat and Gulp Uglify. It would be much more convenient, if the source loading can be done depending on the environment. First step was to replace the SCRIPT and LINK tags in die index.html with a dynamic loading approach using JavaScript. Dynamic JS loadingFor some custom code it was necessary to load the plugins previously, because of dependencies. 123456789101112131415161718function addScriptAsync(url) { return new Promise(function(resolve, reject) { var script = document.createElement("script"); script.type = "text/javascript"; script.src = url; script.addEventListener("load", function() { resolve(script); }, false); script.addEventListener("error", function() { reject(script); }, false); document.getElementsByTagName('head')[0].appendChild(script); });} By returning a Promise, the calling code is able to wait for a dependent source to load: 123addScriptAsync("Build/vendor.min.js").then(function() { addScriptAsync("Build/custom.min.js");}); Dynamic CSS loadingLoading CSS is pretty straightforward and includes an id as parameter, in order to be able to access the style afterwards, for example when tehh user is chanhing the SPA’s theme: 12345678910function addStylesheet(url, id) { var stylesheet = document.createElement('link'); stylesheet.rel = 'stylesheet'; stylesheet.type = 'text/css'; stylesheet.href = url; if (id) { stylesheet.setAttribute("id", id); } document.getElementsByTagName('head')[0].appendChild(stylesheet);} 12addStylesheet("Build/vendor.css");addStylesheet("Build/custom.css"); Consider the environmentNow everything was set up to implement a switch, depending on whether the SPA was started locally or in production. 12345678910111213141516171819202122var _DEV = (window.location.hostname.indexOf("localhost") !== -1);addStylesheet("Build/vendor.css");if (_DEV) { addStylesheet("Libraries/styles.css"); addStylesheet("Libraries/helpers.css"); ...} else { addStylesheet("Build/custom.css");}addScriptAsync("Build/vendor.min.js").then(function() { if (_DEV) { addScriptAsync("Libraries/prototypes.js") .then(function() { return addScriptAsync("Libraries/tools.js"); }) .then(function() { return addScriptAsync("Libraries/app.js"); }) ... } else { return addScriptAsync("Build/custom.min.js"); }})","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"SPA","slug":"SPA","permalink":"https://kiko.io/tags/SPA/"},{"name":"Bundling","slug":"Bundling","permalink":"https://kiko.io/tags/Bundling/"}]},{"title":"Show related posts in Hexo","subtitle":null,"series":"A New Blog","date":"2020-10-03","updated":"2020-10-03","path":"post/Show-related-posts-in-Hexo/","permalink":"https://kiko.io/post/Show-related-posts-in-Hexo/","excerpt":"It is always nice to point the readers of your blog’s articles to related posts, they might be interested in. They stay a little longer to understand what you have to offer and increases the likelihood that they become loyal readers, followers or subscribers. Related posts has become a standard on delivering news and posts. In the default Hexo theme Landscape, on which this blog is based, there is no such function built in, but as the Hexo community is very busy, there are some plugins you can use.","keywords":"nice point readers blogs articles related posts interested stay longer understand offer increases likelihood loyal followers subscribers standard delivering news default hexo theme landscape blog based function built community busy plugins","text":"It is always nice to point the readers of your blog’s articles to related posts, they might be interested in. They stay a little longer to understand what you have to offer and increases the likelihood that they become loyal readers, followers or subscribers. Related posts has become a standard on delivering news and posts. In the default Hexo theme Landscape, on which this blog is based, there is no such function built in, but as the Hexo community is very busy, there are some plugins you can use. Plugin: hexo-list-related-postsThis plugin, available at GitHub is pretty lean and generates a list of links to related posts based on tags. It just counts how often a tag is occuring and shows a list of related posts either by count descending or randomly. Advantage: Easy and fast Disadvantage: Necessity of a sophisticated tag system Technical approach Plugin: hexo-related-postsSergey Zwezdin made much more effort in his solution. The plugins depends on statistic methodologies like Stemming and TF/IDF, provided by the Node library Natural. It has plenty setting options like weighting and reserved words in order to optimize results. Advantages: Much better results Disadvantages: Huge installation, because of many dependent Node modules Necessity of maintaining reserved words Technical approach Manually CuratedOne point, that no technical solution can achieve is: you can guide the reader through your blog, by pointing out posts, which doesn’t really belong to the topic, but tries to give him a wider perspective on your thoughts or work. This is only possible, if you link the related posts manually. Here is a way to implement the requirements… The right place to store related posts is in the Frontmatter of your article. Create a list below the keyword related and take the slug (name of the post file) of the posts you want to show below the article as entries: 12345title: My New fancy Postrelated: - my-other-post - one-of-my-first-posts - yet-another-post In your article.ejs add a new partial called related to the place where it should be shown under the content of the actual article: 123456789101112131415161718192021<article id="<%= post.layout %>-<%= post.slug %>" class="article article-type-<%= post.layout %>" itemscope itemprop="blogPost"> ... <div class="article-inner"> <%- post.content %> </div> <% if (!index){ %> <!-- NEW RELATED PARTIAL --> <%- partial('post/related') %> <%- partial('post/comments') %> <%- partial('post/nav') %> <% } %></article> In the folder themes/landscape/layout/_partial/post, where all partials are stored which belongs to posts, create the new partial file: related.ejs12345678910111213141516171819202122232425<% if (post.related && post.related.length){ %> <div class="article-related"> <h2>Related</h2> <div class="archives"> <!-- Loop through the Frontmatter list of RELATED posts --> <% post.related.forEach(function(item) { %> <!--Determine the post(s) with the given slug --> <% var posts = site.posts.filter(function(post) { return post.slug.toLowerCase() === item.toLowerCase(); }); %> <!-- Loop through the post(s) and render the archive panel --> <% posts.each(function(post) { %> <%- partial('../archive-post', { post: post, show_link: true }) %> <% }); %> <% }); %> </div> </div><% } %> (Remove the comments, because they doesn’t belong to EJS) In this partial we loop through the Frontmatter list of related posts, determine the post by the given slug and render an archive panel for each post. The list site.posts should always contain a slug just once, therefore getting an array of posts and looping is just a precuation. What you are getting you can see below…","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"Discoveries #3 - Tutorials","subtitle":null,"series":"Discoveries","date":"2020-09-29","updated":"2020-09-29","path":"post/Discoveries-3-Tutorials/","permalink":"https://kiko.io/post/Discoveries-3-Tutorials/","excerpt":"Some articles I stumble upon in my daily routine of reading news and blogs are diving very deep in a certain topic, especially if they are describing the basics of techniques I use every day. All of the following reading tips are of the type “ahh, that’s why this works like that” or “uuh, I just scatch on the surface on that”. Take your time and read the articles in detail. We all never stop learning and it’s a pleasure to do so… CSS CSS Viewport UnitsGrid for layout, Flexbox for componentsHow CSS Perspective WorksLinearly Scale font-size with CSS clamp() Based on the ViewportLearn CSS Centering JavaScript The Flavors of Object-Oriented Programming (in JavaScript)Understanding the Event Loop, Callbacks, Promises, and Async/Await in JavaScript","keywords":"articles stumble daily routine reading news blogs diving deep topic describing basics techniques day tips type ahh works uuh scatch surface time read detail stop learning pleasure so… css viewport unitsgrid layout flexbox componentshow perspective workslinearly scale font-size clamp based viewportlearn centering javascript flavors object-oriented programming javascriptunderstanding event loop callbacks promises async/await","text":"Some articles I stumble upon in my daily routine of reading news and blogs are diving very deep in a certain topic, especially if they are describing the basics of techniques I use every day. All of the following reading tips are of the type “ahh, that’s why this works like that” or “uuh, I just scatch on the surface on that”. Take your time and read the articles in detail. We all never stop learning and it’s a pleasure to do so… CSS CSS Viewport UnitsGrid for layout, Flexbox for componentsHow CSS Perspective WorksLinearly Scale font-size with CSS clamp() Based on the ViewportLearn CSS Centering JavaScript The Flavors of Object-Oriented Programming (in JavaScript)Understanding the Event Loop, Callbacks, Promises, and Async/Await in JavaScript CSS Viewport Units by Ahmad Shadeed https://ishadeed.com/article/viewport-units Ahmad is a true master of CSS and describes complex topics in an understandable way. Here he deals with the different Viewport Units: how they are calculated and how to use them properly. Grid for layout, Flexbox for components by Ahmad Shadeed https://ishadeed.com/article/grid-layout-flexbox-components Another one from Ahmad. Here he talks about the usage of Grid and/or Flexbox. Both techniques have their purpose and he shows when to use this or that. How CSS Perspective Works by Amit Sheen https://css-tricks.com/how-css-perspective-works Amit shows in this tutorial how to deal with perspective on using transform and animation in CSS. A true eye opener… Linearly Scale font-size with CSS clamp() Based on the Viewport by Pedro Rodriguez https://css-tricks.com/linearly-scale-font-size-with-css-clamp-based-on-the-viewport Few of us really deal with repsonsive typography. We fiddle arounf with line-height and font-size to achieve an B+ effect. Pedro shows how do it right with clamp() … and it is amazing. Centering in CSS by Ahmad Shadeed https://ishadeed.com/article/learn-css-centering Ahmad again (I told you, he is amazing). In this tutorial he goes through every technique to center stuff in CSS. Never again google ‘center text flexbox’… The Flavors of Object-Oriented Programming (in JavaScript) by Zell Liew https://css-tricks.com/the-flavors-of-object-oriented-programming-in-javascript There are different methods to ‘organize’ your JavaScript code. Zell shows the possibilities and pitfalls of techniques like Constructor Functions, Classes, Factory Functions and OLOO. Huge post, but couldn’t stop reading… Understanding the Event Loop, Callbacks, Promises, and Async/Await in JavaScript by Tania Rascia https://www-digitalocean-com.cdn.ampproject.org/v/s/www.digitalocean.com/community/tutorials/understanding-the-event-loop-callbacks-promises-and-async-await-in-javascript.amp?usqp=mq331AQFKAGwASA%3D&_js_v=0.1 Tanias deep knowledge of asynchronous JavaScript techniques and its basics is as long as this tutorials title and its Url. A must-read.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"Tutorial","slug":"Tutorial","permalink":"https://kiko.io/tags/Tutorial/"}]},{"title":"Device Class Detection in JavaScript","subtitle":"The unusual way by using CSS Media Queries","date":"2020-09-28","updated":"2020-09-28","path":"post/Device-Class-Detection-in-JavaScript/","permalink":"https://kiko.io/post/Device-Class-Detection-in-JavaScript/","excerpt":"In some occasions it is necessary to know which device a user is using while writing JavaScript Web Apps. Should be nothing regarding layout, because for this we have CSS Media Queries. Somewhere around 2011 W3C introduced matchMedia(), which returns a MediaQueryList object that can be used to detemnine if the document matches the media query string. The using is pretty straightforward and feels a bit like RegEx matching in JS: 1234const mediaQuery = window.matchMedia('(min-width: 1025px)')if (mediaQuery.matches) { // do something... } If you are interested in this API, you will find good introductions to the topic here, here and here (German). One point of criticism on this pure JS approach can be, that you have to maintain the breakpoints in addition to CSS … but why not use these existing breakpoints in JS?","keywords":"occasions device user writing javascript web apps layout css media queries w3c introduced matchmedia returns mediaquerylist object detemnine document matches query string pretty straightforward feels bit regex matching js 1234const mediaquery = windowmatchmedia'min-width 1025px'if mediaquerymatches { // } interested api find good introductions topic german point criticism pure approach maintain breakpoints addition … existing","text":"In some occasions it is necessary to know which device a user is using while writing JavaScript Web Apps. Should be nothing regarding layout, because for this we have CSS Media Queries. Somewhere around 2011 W3C introduced matchMedia(), which returns a MediaQueryList object that can be used to detemnine if the document matches the media query string. The using is pretty straightforward and feels a bit like RegEx matching in JS: 1234const mediaQuery = window.matchMedia('(min-width: 1025px)')if (mediaQuery.matches) { // do something... } If you are interested in this API, you will find good introductions to the topic here, here and here (German). One point of criticism on this pure JS approach can be, that you have to maintain the breakpoints in addition to CSS … but why not use these existing breakpoints in JS? If you implement a feature that is based on the different device classes, you don’t have to determine the current class with dozens of lines of JavaScript code, if you just can ask the DOM. The CSS/JS Breakpoint HackFor this approach, we take advantage of the fact, that CSS can be used to define not only styles, but also content. We always use it, when showing an icon by using a symbol font like FontAwesome: 1234my-fancy-icon::before { font-family: FontAwesome5Solid; content: "\\f186";} Mixed with a @media rule, we can “inject” the needed device value into the DOM, for example into the BODY tag, but you can take whatever you want: 12345@media (min-width: 1025px) { body:before { content: "DESKTOP"; }} Just one line more in the masses of CSS code to make a Web App responsive, but with this one you can do without many lines of JS. Now you can read out this value via JavaScript by getting the styles of the tag and get the injected content: 12var style = window.getComputedStyle(document.querySelector("body"), ":before");var breakpoint = style.getPropertyValue("content").replace(/\\"/g, ""); It is advisable to embed this request into an event listener of DOMContentLoaded, because the rule has to be set, before you can access it. See a simple working pen:","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"},{"name":"MediaQuery","slug":"MediaQuery","permalink":"https://kiko.io/tags/MediaQuery/"}]},{"title":"404 Page in Hexo for GitHub Pages","subtitle":"Provide an error page automatically when resource not found","series":"A New Blog","date":"2020-09-23","updated":"2020-09-23","path":"post/404-Page-in-Hexo-for-GitHub-Pages/","permalink":"https://kiko.io/post/404-Page-in-Hexo-for-GitHub-Pages/","excerpt":"As this blog is a static one, generated by Hexo and hostet at GitHub, the page which was shown, when a user enters an Url which points to nowhere, was the default GitHub 404 page.","keywords":"blog static generated hexo hostet github page shown user enters url points default","text":"As this blog is a static one, generated by Hexo and hostet at GitHub, the page which was shown, when a user enters an Url which points to nowhere, was the default GitHub 404 page. Not optimal and should be solved by an own Hexo page, because GitHub Pages allows you to deliver a custom 404 page by creating simply a 404.html in the root of the website. As you can create separate pages in Hexo, this is done quickly by: 1hexo new page "404" It generates a new folder named 404 in your source folder, where a index.md is placed. In this file you can enter the text as Markdown you want to show to the user, in case of a 404 error (page not found) occurs. On generating the static files by hexo generate, a subfolder 404 with a index.html will be created, which doesn’t really work with GitHub Pages, because it needs a 404.htm in the root. You can fix this, by defining the permalink in the Frontmatter of your page: 1234---title: 404permalink: /404.html--- Example … click here: https://kiko.io/no-page-here","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Error","slug":"Error","permalink":"https://kiko.io/tags/Error/"}]},{"title":"Pimping the Permalink","subtitle":"How to copy and share the permalink programatically","series":"A New Blog","date":"2020-09-20","updated":"2020-09-20","path":"post/Pimping-the-Permalink/","permalink":"https://kiko.io/post/Pimping-the-Permalink/","excerpt":"Until now I did not show the permalink under my posts in this blog, but in the past I had sometimes the need to pass one of the links and it was not very user-friendly, on desktop as well as on mobile. Not the One-Click experience I prefer. My goal was to show the permalink and, even more important, provide a simple way to copy and to share. JavaScript to the rescue…","keywords":"show permalink posts blog past pass links user-friendly desktop mobile one-click experience prefer goal important provide simple copy share javascript rescue…","text":"Until now I did not show the permalink under my posts in this blog, but in the past I had sometimes the need to pass one of the links and it was not very user-friendly, on desktop as well as on mobile. Not the One-Click experience I prefer. My goal was to show the permalink and, even more important, provide a simple way to copy and to share. JavaScript to the rescue… DisplayAs I run my blog with Hexo, I deal with EJS files. To show the permalink in my article.ejs, was quite simple. First step was to create a new partial file named permalink.ejs, to be called every time when the complete article has to be rendered: 123<% if (!index){ %> <%- partial('post/permalink', { class_name: 'article-permalink' }) %><% } %> The partial file looked like this in this step: 123<div class="<%= class_name %>""> <a id="article-permalink" href="<%- post.permalink %>"><%- post.permalink %></a></div> CopyAs I read a little bit about the possibilities to copy text into the clipboard via JavaScript on MDN, it became obvious that a link is not the best solution, because using the exeCommand needs to have something selected and this is difficult on anchors. Then … do it with an input: 123456789101112131415161718192021222324252627<div class="<%= class_name %>""> <input id="article-permalink" value="<%- post.permalink %>" /> <a id="action-copy" class="article-action" href="javascript:copyPermalink();"></a></div><script> var copyText = document.querySelector("#article-permalink"); //Disable Input by default copyText.disabled = true; function copyPermalink() { //Enable Input copyText.disabled = false; //Select permalink text copyText.select(); //Copy to clipboard document.execCommand("copy"); //Remove selection again copyText.blur(); //Disable Input again copyText.disabled = true; }</script> Nice, but a user feedback, that the text has been copied to the clipboard, was advisable, because nothing is more annoying, when you click somewhere and nothing seems to happen. As I hate default browser confirmations and other distracting messaging methods, I wanted to use the input itself, by fading out the link text, replace it with a message and fade in the text again: I extended my animation.styl (Hexo works with Stylus) with two keyframe animations … one for fading in, one for fading out… 12345678910111213141516171819202122232425@keyframes fadeIn { 0% { opacity:0; } 100% { opacity:1; }}.fade-in-500 animation: fadeIn ease 0.5s;.fade-in-1000 animation: fadeIn ease 1s;@keyframes fadeOut { 0% { opacity:1; } 100% { opacity:0; }}.fade-out-500 animation: fadeOut ease 0.5s;.fade-out-1000 animation: fadeOut ease 1s; … and wrote a setTimeout cascade to achive the effect: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152<div class="<%= class_name %>""> <input id="article-permalink" value="<%- post.permalink %>" /> <a class="article-action action-copy" href="javascript:copyPermalink();"></a></div><script> var copyText = document.querySelector("#article-permalink"); copyText.disabled = true; function copyPermalink() { copyText.disabled = false; copyText.select(); document.execCommand("copy"); copyText.blur(); copyText.disabled = true; //Store original text var permalink = copyText.value; //Start fading out copyText.classList.add("fade-out-500"); //Wait until animation is done setTimeout(function(){ //Set message, remove fadout class and add start fading in copyText.value = "copied to clipboard"; copyText.classList.remove("fade-out-500"); copyText.classList.add("fade-in-1000"); //Wait 2 seconds to show the message setTimeout(function() { //Start to fade out message copyText.classList.add("fade-out-500"); //Wait until animation is done setTimeout(function() { //Set original text again and remove fadout class copyText.value = permalink; copyText.classList.remove("fade-out-500"); //Wait until animation is done setTimeout(function() { //Remove fadeout class copyText.classList.remove("fade-in-1000"); }, 500); }, 500); }, 2000); }, 500); }</script> ShareThe second permalink feature was a little bit trickier, because I didn’t want to use one of the sharing libraries out there, whose business model is based on my readers data (always keep conservative on implementing third party stuff, because you never know what they are doing with the data). But a couple of months ago I read about a new native browser API for WebApps on the rise: Web Share API. Since 2019 W3C is working on this API, for sharing text, links and other content to an arbitrary destination of the user’s choice. On 27 August 2020 the published a Working Draft and on 16 September 2020 the latest Editors Draft. Brand new stuff. The browser support is not the best yet, but it will be getting better in the near feature, especially as Edge Chrome is one of the early adopters. web.dev lists important requirements on using this new feature in JavaScript: It can only be used on a site that supports HTTPS It must be invoked in response to a user action such as a click But it can share URL’s, text and even files! A raw implementation can be: 12345678910if (navigator.share === undefined) { navigator.share({ title: 'My Post', url: 'https://my-domain.com/my-url', }) .then(() => console.log('Successful share')) .catch((error) => console.log('Error sharing', error));} else { // fallback} I refrain to implement a fallback, rather I would like to show the appropriate button only to those users, whose browser supports it: 12345678910111213141516171819202122<div class="<%= class_name %>""> <input id="article-permalink" value="<%- post.permalink %>" data-id="<%= post._id %>" /> <a id="action-copy" class="article-action" href="javascript:copyPermalink();"></a> <a id="action-share" class="article-action" href="javascript:sharePermalink();"></a></div><script> function copyPermalink() { -- SEE ABOVE } if (navigator.share === undefined) { var shareLink = document.querySelector("#action-share"); shareLink.style.display = "none"; } function sharePermalink() { navigator.share({ title: "<%- post.title %>", url: "<%- post.permalink %>", }) }</script> More Info w3c.github.io: W3C Web Share Testheise Developer: Features von übermorgen: Die Web Share API und die Web Share Target API (German)CSS-Tricks: How to Use the Web Share API","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Stylus","slug":"Stylus","permalink":"https://kiko.io/tags/Stylus/"},{"name":"Share","slug":"Share","permalink":"https://kiko.io/tags/Share/"}]},{"title":"Discoveries #2","subtitle":null,"series":"Discoveries","date":"2020-09-07","updated":"2020-09-07","path":"post/Discoveries-2/","permalink":"https://kiko.io/post/Discoveries-2/","excerpt":"New month, new discoveries. We will deal with key bindings, downloads on the fly, a lot of animations and contrasting images. Have fun, trying out these stunning solutions. tinykeys - Modern library for keybindingsCreating files in JavaScript in your browserCSS Animated Google FontsSkeleton Screen CSSMore Control Over CSS Borders With background-imageA CSS-only, animated, wrapping underlineNailing the Perfect Contrast Between Light Text and a Background ImageContrast.js","keywords":"month discoveries deal key bindings downloads fly lot animations contrasting images fun stunning solutions tinykeys - modern library keybindingscreating files javascript browsercss animated google fontsskeleton screen cssmore control css borders background-imagea css-only wrapping underlinenailing perfect contrast light text background imagecontrastjs","text":"New month, new discoveries. We will deal with key bindings, downloads on the fly, a lot of animations and contrasting images. Have fun, trying out these stunning solutions. tinykeys - Modern library for keybindingsCreating files in JavaScript in your browserCSS Animated Google FontsSkeleton Screen CSSMore Control Over CSS Borders With background-imageA CSS-only, animated, wrapping underlineNailing the Perfect Contrast Between Light Text and a Background ImageContrast.js tinykeys - Modern library for keybindings by Jamie Kyle https://jamiebuilds.github.io/tinykeys Very easy to use key binding library for JavaScript. Supports key sequences and modifier keys. Creating files in JavaScript in your browser by Kilian Valkhof https://kilianvalkhof.com/2020/javascript/creating-files-in-javascript-in-your-browser Kilian shows how to prepare data in JavaScript and offer them to download on the fly, without the use of storing a file. CSS Animated Google Fonts by Jhey Tompkins https://dev.to/jh3y/animated-google-fonts-193d As Google Fonts now supports variable fonts, Jhey shows a solution how to create neat font animations with them. Skeleton Screen CSS by Dmitriy Kuznetsov https://github.com/nullilac/skeleton-screen-css When loading data on demand, it is sometimes advisable to show placeholders, where the data will be filled in. Dimitriy has founded a CSS framework for these skeletons. More Control Over CSS Borders With background-image by Chris Coyier https://css-tricks.com/more-control-over-css-borders-with-background-image Borders are used to seperate things in a layout, but the build-in possibilities of CSS are restricted. Chris found a way by pimping borders up, using background images. A CSS-only, animated, wrapping underline by Nicky Meuleman https://nickymeuleman.netlify.app/blog/css-animated-wrapping-underline As Chris did for the borders, Nick’s doing on underlined links. An end to boring rigid unterlines, let’s animate them. Nailing the Perfect Contrast Between Light Text and a Background Image by Yaphi Berhanu https://css-tricks.com/nailing-the-perfect-contrast-between-light-text-and-a-background-image Showing text on background images can be challenging due to contrast and readability. Yaphi has developed a solution to find always the right transparent overlay to show the most of the picture, but keep the text readable. Stunning… Contrast.js by Misha Petrov https://github.com/MishaPetrov/Contrast.js Misha addresses the same problem as Yaphi, showing text on background images, but goes a different way with his library, which is trying to find the best constrasting text color, even if the page is responsive.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Add website to Trello card the better way","subtitle":"Avoid default share, use the Trello bookmarklet","date":"2020-09-07","updated":"2020-09-07","path":"post/Add-website-to-Trello-card-the-better-way/","permalink":"https://kiko.io/post/Add-website-to-Trello-card-the-better-way/","excerpt":"I was looking for a new way to store interesting website articles for reading later, as Pocket, my favourite tool until here, gets worse and worse. As I am a big Trello fan, I wanted to give it a chance to be Pockets successor on my smartphone, where I’m reading mostly. On installing the Trello Android app, you will find a new SHARE target Add new Trello card, which is comfortable to use: (Sry, for the German screenshots ;) The result, website’s title and Url set, is nice at best: … but Trello has a Bookmarklet, which does the job much better.","keywords":"store interesting website articles reading pocket favourite tool worse big trello fan wanted give chance pockets successor smartphone im installing android app find share target add card comfortable sry german screenshots result websites title url set nice … bookmarklet job","text":"I was looking for a new way to store interesting website articles for reading later, as Pocket, my favourite tool until here, gets worse and worse. As I am a big Trello fan, I wanted to give it a chance to be Pockets successor on my smartphone, where I’m reading mostly. On installing the Trello Android app, you will find a new SHARE target Add new Trello card, which is comfortable to use: (Sry, for the German screenshots ;) The result, website’s title and Url set, is nice at best: … but Trello has a Bookmarklet, which does the job much better. The following approach works best in the Google Chrome browser. First, a Bookmarklet is a small piece of JavaScript, which is stored as a bookmark in your browser. As you can’t actually create such a Bookmarklet in your Android Chrome, you have to create it in your desktop Chrome and switch on the bookmark sync of chrome. You should right away choose a short, concise name for the bookmark, so you find it easier in Android Chrome afterwards. I called it 2TrelloCard, because few websites start with an number. After Chrome’s sync is done, go to any website do you want to store as a Trello card. Now enter the Url box and type the name of the bookmarklet and select it. Instead of requesting a different page, Chrome executes the JavaScript of the Bookmarklet against the currently open website. This script shows a Trello dialog, where you can choose, which board and list the new card should be created on. This card creation method not only sets the title of the card, but fills the description with the meta description of the page, adds the first found meta image as cover and adds the Url as an attachment:","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Trello","slug":"Trello","permalink":"https://kiko.io/tags/Trello/"},{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"}]},{"title":"Horizontal navigation menu above an image","subtitle":"How to deal with coverage, readability and scrollbars","series":"A New Blog","date":"2020-07-20","updated":"2020-07-20","path":"post/Horizontal-navigation-menu-above-an-image/","permalink":"https://kiko.io/post/Horizontal-navigation-menu-above-an-image/","excerpt":"I changed the main menu of my blog, because I wanted to get rid of the hamburger menu on the upper left, which was shown only for smartphones, but was not really reachable conveniently. Beside that it made no sense to have different navigations for different devices. My choice was to implement a horizontal scrolling menu, which can grow over the time, without any need of customizing. As I have quite big header images and I wanted to place the new navigation in a more accessible zone, I decided to place it at the bottom, but above the header image.","keywords":"changed main menu blog wanted rid hamburger upper left shown smartphones reachable conveniently made sense navigations devices choice implement horizontal scrolling grow time customizing big header images place navigation accessible zone decided bottom image","text":"I changed the main menu of my blog, because I wanted to get rid of the hamburger menu on the upper left, which was shown only for smartphones, but was not really reachable conveniently. Beside that it made no sense to have different navigations for different devices. My choice was to implement a horizontal scrolling menu, which can grow over the time, without any need of customizing. As I have quite big header images and I wanted to place the new navigation in a more accessible zone, I decided to place it at the bottom, but above the header image. Problem was, not to cover a big part of the image with a full-colored or even semitransparent bar, by using a RGBA background color. I wanted it more translucent, but with enough contrast on bright images for the menu items to read. The recently introduced W3C feature backdrop-filter was just the right thing for that. It is supported by most modern browsers, but it has to have a backup strategy for the rest of the bunch. The HTML is simple: 1234567891011121314151617<nav id="header-nav" role="navigation"> <ul class="menu"> <li class="menu-item"> <a href="/first" title="First"> <span>First Item</span> </a> </li> <li class="menu-item"> <a href="/second" title="Second"> <span>Second Item</span> </a> </li> </ul></nav> And here’s the Stylus code for my approach: 123456789101112131415161718192021222324252627282930313233343536373839404142#header-nav position: absolute bottom: 0 width: 100% height: auto box-sizing: content-box overflow-x: scroll overflow-y: hidden // BACKDROP-FILTER backdrop-filter: blur(5px) brightness(90%) @supports not (backdrop-filter: none) background: rgba(0,0,0,0.25) // SCROLLBAR &::-webkit-scrollbar display: none @supports not (webkit-scrollbar) scrollbar-width: none .menu display: flex list-style: none margin: 0 padding: 0 .menu-item flex-basis: 80px flex-shrink: 0 flex-grow: 1 max-width: 100px margin: 0 2px text-overflow: ellipsis; a display: inline-block width: 100% padding: 10px 0 color: #ffffff font-weight: bold text-decoration: none text-align: center The navigation box is absolute positioned on the image, is as wide as the screen and scrolls exclusively horizontal. The items are a unordered list, with default width and arranged by flex. In case a browser doesn’t understand backdrop-filter, the navigation bar is shown with a classic alpha channel opacity. When having a horizontal scroll feature, the scrollbar shown by the browser is beyond beautiful. To prevent this, I used the CSS pseudo element ::-webkit-scrollbar, which is supported by WebKit and Blink bowsers, with a fallback for all other browsers. Both strategies allows to be still able to scroll. If you want to have a scrollbar, but not the built-in, I can only recommend to read something about styling scrollbars, like here and here.","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Stylus","slug":"Stylus","permalink":"https://kiko.io/tags/Stylus/"}]},{"title":"Change CSS class when element scrolls into viewport","subtitle":null,"series":"A New Blog","date":"2020-07-13","updated":"2020-07-13","path":"post/Change-CSS-class-when-element-scrolls-into-viewport/","permalink":"https://kiko.io/post/Change-CSS-class-when-element-scrolls-into-viewport/","excerpt":"I had a neat visual gimmick on the start page of this blog, that the gray-scaled header image of a post in the list scaled up to 100% and became colored, when the user hovered over it: 123456789101112131415.article-inner .article-photo { height: 150px; width: 100%; object-fit: cover; transform: scale(1); transform-style: preserve-3d; transition: all ease-out 0.6s; opacity: 0.3; filter: grayscale(1) contrast(0.5);}.article-inner:hover .article-photo { transform: scale(1.1); opacity: 1; filter: grayscale(0) contrast(1);} Nice, but a little bit useless on smartphones or tablets, where HOVER doesn’t really work.","keywords":"neat visual gimmick start page blog gray-scaled header image post list scaled 100% colored user hovered 123456789101112131415article-inner article-photo { height 150px width object-fit cover transform scale1 transform-style preserve-3d transition ease-out 06s opacity filter grayscale1 contrast05}article-innerhover scale11 grayscale0 contrast1} nice bit useless smartphones tablets hover doesnt work","text":"I had a neat visual gimmick on the start page of this blog, that the gray-scaled header image of a post in the list scaled up to 100% and became colored, when the user hovered over it: 123456789101112131415.article-inner .article-photo { height: 150px; width: 100%; object-fit: cover; transform: scale(1); transform-style: preserve-3d; transition: all ease-out 0.6s; opacity: 0.3; filter: grayscale(1) contrast(0.5);}.article-inner:hover .article-photo { transform: scale(1.1); opacity: 1; filter: grayscale(0) contrast(1);} Nice, but a little bit useless on smartphones or tablets, where HOVER doesn’t really work. A better idea was to transform the header image automatically, when it becomes visible to the user. So I changed the HOVER selector into a class… 12345.article-photo.in-view { transform: scale(1.1); opacity: 1; filter: grayscale(0) contrast(1);} … and wrote a little JS function to determine the point, where the images is fully visible in the viewport: 1234567function isVisibleInViewPort(e) { var viewTop = $(window).scrollTop(); var viewBottom = viewTop + $(window).height(); var eTop = $(e).offset().top; var eBottom = eTop + $(e).height(); return ((eBottom <= viewBottom) && (eTop >= viewTop));} This function I had to bind to the windows scroll event to all header images only: 123456789$(window).on('scroll', function() { $(".article-photo").each(function() { if (isVisibleInViewPort($(this))) { $(this).addClass("in-view"); } else { $(this).removeClass("in-view"); } });});","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"jQuery","slug":"jQuery","permalink":"https://kiko.io/tags/jQuery/"},{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"}]},{"title":"Discoveries #1","subtitle":null,"series":"Discoveries","date":"2020-07-12","updated":"2020-07-12","path":"post/Discoveries-1/","permalink":"https://kiko.io/post/Discoveries-1/","excerpt":"Due to my daily routine, I’m reading a lot of articles on the web regarding software development. The most interesting stuff ends up on my Pocket list, which grows from day to day. Hard to find the pearls, when I need them. This recurring posts will throw a stroke of light on them. They are maybe not the newest finds, not the fanciest ones, but remarkable for me and maybe for you also. Pure CSS halftone portrait from .jpg sourceScrollTrigger - Highlight TextTiny long-press event handlerShow More/Less3D banners with ScrollTriggerImage Compare ViewerAdd Read or Scroll Progress Bar To A Website To Indicate Read ProgressHow to Get a Progressive Web App into the Google Play Store","keywords":"due daily routine im reading lot articles web software development interesting stuff ends pocket list grows day hard find pearls recurring posts throw stroke light newest finds fanciest remarkable pure css halftone portrait jpg sourcescrolltrigger - highlight texttiny long-press event handlershow more/less3d banners scrolltriggerimage compare vieweradd read scroll progress bar website progresshow progressive app google play store","text":"Due to my daily routine, I’m reading a lot of articles on the web regarding software development. The most interesting stuff ends up on my Pocket list, which grows from day to day. Hard to find the pearls, when I need them. This recurring posts will throw a stroke of light on them. They are maybe not the newest finds, not the fanciest ones, but remarkable for me and maybe for you also. Pure CSS halftone portrait from .jpg sourceScrollTrigger - Highlight TextTiny long-press event handlerShow More/Less3D banners with ScrollTriggerImage Compare ViewerAdd Read or Scroll Progress Bar To A Website To Indicate Read ProgressHow to Get a Progressive Web App into the Google Play Store Pure CSS halftone portrait from .jpg source by Ana Tudor https://codepen.io/thebabydino/pen/LYGGwrm Ana, author at CSS Tricks, shows a CSS-only technique to convert an image into a halftone one. ScrollTrigger - Highlight Text by Ryan Mulligan https://codepen.io/hexagoncircle/details/gOPMwvd We all highlight important text passages for our readers. Ryan does the in an unusual, butt cool way by using GSAP ScrollTrigger. Tiny long-press event handler by MudOnTire https://github.com/MudOnTire/web-long-press Vanilla JS multi-instance handling of long press event the easy way. Show More/Less by Grzegorz Tomicki https://github.com/tomik23/show-more Grzegorz’s little JS helper to cut texts, lists and even tables and show a MORE link. 3D banners with ScrollTrigger by supamike https://codepen.io/supamike/full/KKVqXmR Awesome 3D effect on scrolling made with ScrollTrigger. Image Compare Viewer by Kyle Wetton https://image-compare-viewer.netlify.app/ Comparison slider in Vanilla JS to compare BEFORE and AFTER images, which works responsively on every device. Add Read or Scroll Progress Bar To A Website To Indicate Read Progress by Jun711 https://jun711.github.io/web/add-scroll-progress-bar-to-a-website-to-indicate-read-progress/ A classic, simply explained… Here another approach: CSS Tricks: Reading Position Indicator How to Get a Progressive Web App into the Google Play Store by Mateusz Rybczonek https://css-tricks.com/how-to-get-a-progressive-web-app-into-the-google-play-store/ Mateusz describes very detailed how offer your PWA as an App via Google Play Store.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Dopamine: How Software should be...","subtitle":"A great media player for Windows 10","series":"Great Finds","date":"2020-07-10","updated":"2020-07-10","path":"post/Dopamine-How-Software-should-be/","permalink":"https://kiko.io/post/Dopamine-How-Software-should-be/","excerpt":"Not very often, when I’m looking for a new tool to replace some annoying or outdated piece of software, I have to blog about it … but from time to time, I’m discovering pearls, worth to lose a word about. The Windows 10 built-in media player Groove is (to be kind) … nice, but it is more or less a leftover from Microsoft’s attempt to create a competitor to iTunes, years ago. The crippeled UI is not the most modern and I was more than once annoyed about its usability. Doing a research for a good alternative, you stumble always over the usual suspects: MediaMonkey, Foobar2000, Winamp, VLC or even Media Player Classic!? Not modern enough, not user friendly enough, not lean enough. I really don’t remember where, but there was a screenshot of a player, which seems to be the complete opposite of the others: Dopamine from Digimezzo, a project by the Belgian developer Raphaël Godart…","keywords":"im tool replace annoying outdated piece software blog … time discovering pearls worth lose word windows built-in media player groove kind nice leftover microsofts attempt create competitor itunes years ago crippeled ui modern annoyed usability research good alternative stumble usual suspects mediamonkey foobar2000 winamp vlc classic user friendly lean dont remember screenshot complete opposite dopamine digimezzo project belgian developer raphaël godart…","text":"Not very often, when I’m looking for a new tool to replace some annoying or outdated piece of software, I have to blog about it … but from time to time, I’m discovering pearls, worth to lose a word about. The Windows 10 built-in media player Groove is (to be kind) … nice, but it is more or less a leftover from Microsoft’s attempt to create a competitor to iTunes, years ago. The crippeled UI is not the most modern and I was more than once annoyed about its usability. Doing a research for a good alternative, you stumble always over the usual suspects: MediaMonkey, Foobar2000, Winamp, VLC or even Media Player Classic!? Not modern enough, not user friendly enough, not lean enough. I really don’t remember where, but there was a screenshot of a player, which seems to be the complete opposite of the others: Dopamine from Digimezzo, a project by the Belgian developer Raphaël Godart… But that wasn’t the best, especially for me. Dopamine is written in C# as a WPF application and it is OpenSource, hosted on GitHub. The software is so wonderful lean and its integrating in Windows 10 like a charm. It has several categories to find the right music to play, a context-sensitive search, a folder view, is able to import and manage playlists, a light and dark mode and translations into currently 28 languages. It can update your collection automatically from several folders, has two player modes and is incredibly fast. The keep long story short … I fell in love on Dopamine‘s simple beauty and it is now my favourite player on Windows 10! Thanks Raphaël…","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Audio","slug":"Audio","permalink":"https://kiko.io/tags/Audio/"}]},{"title":"Using GitHub as Commenting Platform","subtitle":"Integrate Utterances' GitHub Issue Commenting to Hexo","series":"A New Blog","date":"2020-07-05","updated":"2020-07-05","path":"post/Using-GitHub-as-Commenting-Platform/","permalink":"https://kiko.io/post/Using-GitHub-as-Commenting-Platform/","excerpt":"If you run a blog, it is always advisable to integrate a commenting system, in order to get feedback on your posts from your readers. So did I, when I start this blog and I decided to use the Disqus platform, as it was very easy to integrate … but I always had a bad feeling about a third-party platform collecting data from my readers. Disqus is probably not without reason under criticism by many people in the community. As I host this blog at GitHub (see A New Blog (Part One): VS Code, Hexo and GitHub Pages) I was looking for a solution to host the comments also on my prefered platform. There were some solutions, which uses GitHub Issues for this and wanted to implement something like that someday.","keywords":"run blog advisable integrate commenting system order feedback posts readers start decided disqus platform easy … bad feeling third-party collecting data reason criticism people community host github part code hexo pages solution comments prefered solutions issues wanted implement someday","text":"If you run a blog, it is always advisable to integrate a commenting system, in order to get feedback on your posts from your readers. So did I, when I start this blog and I decided to use the Disqus platform, as it was very easy to integrate … but I always had a bad feeling about a third-party platform collecting data from my readers. Disqus is probably not without reason under criticism by many people in the community. As I host this blog at GitHub (see A New Blog (Part One): VS Code, Hexo and GitHub Pages) I was looking for a solution to host the comments also on my prefered platform. There were some solutions, which uses GitHub Issues for this and wanted to implement something like that someday. As I read a post from on Thomas Lavesques’ blog, to solve another problem, his commenting section came to my attention: utteranc.es … exactly the solution I needed! Thanx guys… On their website is a small configurator for a script to implement in each post, which needs only few information: Name of the Repo How the mapping of the post to the Issues should work Name of the Theme, in order to fit to the colors of the blog The script had to be included to my Hexo article.js: 123456789<% if (!index && post.comments){ %> <script src="https://utteranc.es/client.js" repo="kristofzerbe/kiko.io" issue-term="pathname" theme="github-light" crossorigin="anonymous" async> </script><% } %> That’s pretty much it. On entering the first comment, Utterances told me to install the needed GitHub App to my repo, in order to make it work … and done. The result you see below … UPDATE…The utterances script tag has the attribute theme, to tell utterances which style should be delivered. There are several themes available, but if users are able to switch between light or dark mode on the page (see Hexo and the Dark Mode), the comment block should change to an suitable theme also. On order to respond on a mode change, it is necessary to write a more dynamic script loading. First we define a function in a global script file to load the utterances script via JS: 1234567891011121314151617181920function insertUtterancesCommentBlock() { var commentTheme = "github-light"; if(localStorage.getItem("theme") === "dark"){ commentTheme = "github-dark"; } const scriptId = "comment-theme-script"; const existingScript = document.getElementById(scriptId); if (!existingScript) { const commentScript = document.createElement("script"); commentScript.id = scriptId; commentScript.src = "https://utteranc.es/client.js"; commentScript.setAttribute("repo", "kristofzerbe/kiko.io"); commentScript.setAttribute("issue-term", "pathname"); commentScript.setAttribute("theme", commentTheme); commentScript.setAttribute("crossorigin", "anonymous"); const placeholder = document.getElementById("comment-placeholder"); placeholder.innerHTML = ""; placeholder.appendChild(commentScript); }} Then we change the placement in the EJS file, by defining a placeholder and ensuring that the script above is loaded, before we call it: 123456<div id="comment-placeholder"></div><script> window.addEventListener('load', function () { insertUtterancesCommentBlock(); })</script> On my blog, everytime the user switches between light/dark mode the body tag will be decorated with the data tag data-theme and the value of the mode. To keep the loading of the utterances script independent from this functionality, we just have to listen to this change via MutationObserver: 12345678910//observe theme change, to adjust comment block themevar target = document.documentElement, observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.attributionName === "data-theme" ); insertUtterancesCommentBlock(); }); }), config = { attributes: true };observer.observe(target, config);","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"Meaningful automatic versioning with T4","subtitle":"How to implement versioning in C# projects the better way","date":"2020-06-27","updated":"2020-06-27","path":"post/Meaningful-automatic-versioning-with-T4/","permalink":"https://kiko.io/post/Meaningful-automatic-versioning-with-T4/","excerpt":"Every developer has to have an idea of versioning his products. If you work with Visual Studio you have the Assembly Information in the project properties dialog, to enter it manually everytime you want to release a new version: The four fields are: MAJOR, MINOR, BUILD, REVISION. But seriously … who does that? I guess 99% of all C# developers are entering the AssemblyInfo.cs and enter the famous 2 asterisks into the version declaration of BUILD and REVISION, to let Visual Studio do the incrementation job: 12[assembly: AssemblyVersion("1.0.*.*")][assembly: AssemblyFileVersion("1.0.*.*")] But this is not the end of the possibilities … Let’s do it more meaningful, with some goodies and still automatic…","keywords":"developer idea versioning products work visual studio assembly information project properties dialog enter manually everytime release version fields major minor build revision … guess 99% c# developers entering assemblyinfocs famous asterisks declaration incrementation job 12[assembly assemblyversion"10**"][assembly assemblyfileversion"10**"] end possibilities lets meaningful goodies automatic…","text":"Every developer has to have an idea of versioning his products. If you work with Visual Studio you have the Assembly Information in the project properties dialog, to enter it manually everytime you want to release a new version: The four fields are: MAJOR, MINOR, BUILD, REVISION. But seriously … who does that? I guess 99% of all C# developers are entering the AssemblyInfo.cs and enter the famous 2 asterisks into the version declaration of BUILD and REVISION, to let Visual Studio do the incrementation job: 12[assembly: AssemblyVersion("1.0.*.*")][assembly: AssemblyFileVersion("1.0.*.*")] But this is not the end of the possibilities … Let’s do it more meaningful, with some goodies and still automatic… More informative versioningA build with an increased MAJOR version number means, that there are significant changes in the product, even breaking changes. This always should be set manually. Also the MINOR. It stands for significant functional extensions of the product. How does Visual Studio calculate BUILD and REVISION? When specifying a version, you have to at least specify major. If you specify major and minor, you can specify an asterisk for build. This will cause build to be equal to the number of days since January 1, 2000 local time, and for revision to be equal to the number of seconds since midnight local time, divided by 2. But, the BUILD number should explain, how often a software with a particular MAJOR.MINOR has been build, due to minor changes and bug fixes. The “Asterisk” REVISION number is a little weird, but at least with the BUILD number unique. But it says nothing. Better to pick up the idea of a date calculated, unique number, but not an arbitrary date … let’s take the date the project has started. For example: 1.2.16.158 … reads version 1.2 with 16 builds on the 158’th day after the project has started. Start with T4T4 (Text Template Transformation Toolkit) is a templating system in Visual Studio for generating text files during design time. It is very suitable to even generate code. Read about it here and here. A Text Template (.tt) has Directives (how the template is processed), Text blocks (text copied to the output) and Control blocks (program code). For our versioning template, we start with this in a new file named AssemblyVersion.tt: Directives: 12<#@ template hostspecific="true" language="C#" #><#@ output extension=".cs" #> Control block: 123456<# int major = 1; int minor = 0; int build = 0; int revision = 0;#> Text block: 1234567// This code was generated by a tool. Any changes made manually will be lost// the next time this code is regenerated.using System.Reflection;[assembly: AssemblyVersion("<#= $"{major}.{minor}.{build}.{revision}" #>")][assembly: AssemblyFileVersion("<#= $"{major}.{minor}.{build}.{revision}" #>")] On saving the TT file, a new CS file with the same name will be created automatically and you got an error like this: A new place for version infoTh error occurs, because we have now two AssemblyVersion and AssemblyFileVersion attributes in our project. We need to comment out the original in Properties\\AssemblyInfo.cs: Structural ConsiderationsIt makes sense to store all needed files for the new versioning system in a new root folder of the project, named AssemblyVersion, starting with the AssemblyVersion.tt, because there will be more files later on. New app information fileAs we replaced the original version attributes in the project with those from our generated AssemblyVersion.cs, we cannot control the MAJOR and MINOR version number via the project property dialog any longer. We need a new approach on that, which can be edited easily and processed automatically. AssemblyVersion.json1234567891011121314151617{ "initialDate": "2019-09-29", "versions": [ { "major": 1, "minor": 1, "releaseDate": "", "remarks": "Some cool new features; New versioning system" }, { "major": 1, "minor": 0, "releaseDate": "2019-10-01", "remarks": "Initial Release" } ]} This new JSON file has two main items: initialDate - the date the project has started, to calculate the REVISION later on versions - a list with all different MAJOR/MINOR versions we have done so far, with at least one without a release date … the one with the highest major and minor. The remarks attribute of a list item holds some information about the changes in a new version. Together with releaseDate, useful for a possible release history, shown in the product itself. Library references in T4T4 runs in its own app domain, therefore it can use built-in libraries as System.IO, but not third-party libraries like Newtonsoft.JSON. We could reference those libraries from the projects package folder via the absolute path (if we use it in our product), but when we are running a NuGet update, the reference will break. It is advisable to store such libraries directly in a fixed folder, like AssemblyVersion\\Libraries. They won’t have any impact to our product, because the are only used while design time. The MAJOR and MINORTo process the new AssemblyVersion.json in the template, we need some new directives for referencing the needed libraries and the import of the appropriate namepaces: 123456<#@ assembly name="System.Core" #><#@ assembly name="$(SolutionDir)\\AssemblyVersion\\Libraries\\Newtonsoft.Json.dll" #><#@ import namespace="System.IO" #><#@ import namespace="System.Linq" #><#@ import namespace="Newtonsoft.Json" #> Via the use of the T4 variable $(SolutionDir), we can point to our copy of Newtonsoft JSON. Now we can read and convert the JSON into an anonymous object and get the highest values of MAJOR and MINOR: 12345678910111213141516171819202122232425262728<# string avPath = this.Host.ResolvePath("AssemblyVersion.json"); string avJson = File.ReadAllText(avPath); var avDefinition = new { initialDate = "", versions = new [] { new { major = 0, minor = 0, releaseDate = "", remarks = "" } } }; var avObject = JsonConvert.DeserializeAnonymousType(avJson, avDefinition); //Get highest Major/Minor from versions list var maxVersion = avObject.versions .OrderByDescending(i => i.major) .ThenByDescending(j => j.minor) .First(); //Set MAJOR int major = maxVersion.major; //Set MINOR int minor = maxVersion.minor;#> The BuildLogIn order to get the version number for BUILD, we need a method to count and store every build that has been run, separated by the MAJOR/MINOR versions. This is a job for a Post-build event, which can be configured in the project properties dialog. The event uses shell commands as they are used on the command line. What the commands should do: Write a new line with the current date and time in a log file, named after the MAJOR/MINOR version and stored in the folder AssemblyVersion\\BuildLogs. Extending build event macrosShell commands for build events are supporting built-in variables, so called ‘macros’, like $(ProjectDir) (which returns the project directory path), but there is no such macro for the current version number. We have to introduce it via extending the project with a new build target. Unload the project in Visual Studio for editing the CSPROJ (or VBPROJ) file of your product manually and write the following definition just before the end-tag: 123456789101112131415<PropertyGroup> <PostBuildEventDependsOn> $(PostBuildEventDependsOn); PostBuildMacros; </PostBuildEventDependsOn></PropertyGroup><Target Name="PostBuildMacros"> <GetAssemblyIdentity AssemblyFiles="$(TargetPath)"> <Output TaskParameter="Assemblies" ItemName="Targets" /> </GetAssemblyIdentity> <ItemGroup> <VersionNumber Include="@(Targets->'%(Version)')" /> </ItemGroup></Target> After reloading the project in Visual Studio, we can use @(VersionNumber) in our commands. CreateBuildLog.batThe event build editor is not very comfortable, so we create the batch file CreateBuildLog.bat in our AssemblyVersion folder and use this as the post build event command. The BuildLog folder must exist, before running the following command the first time! 123456789101112131415161718192021222324252627@echo offREM --Get parametersset PROJECT_DIR=%1set VERSION_NUMBER=%2REM --Set what to logset LOG_LINE=%DATE% %TIME%REM --Inform the userset MSG=CreateBuildLog '%LOG_LINE%' for version %VERSION_NUMBER%echo %MSG%REM --Get version partsFOR /f "tokens=1,2,3,4 delims=." %%a IN ("%VERSION_NUMBER%") do ( set MAJOR=%%a set MINOR=%%b set BUILD=%%c set REVISION=%%d)REM --Define BuildLog file and folder set BUILDLOG_FILE=%MAJOR%.%MINOR%.logset BUILDLOG_FOLDER=%PROJECT_DIR%\\AssemblyVersion\\BuildLogsREM --Write current date and time as new line in the fileecho %LOG_LINE% >> %BUILDLOG_FOLDER%\\%BUILDLOG_FILE%" 1"$(ProjectDir)\\AssemblyVersion\\CreateBuildLog.bat" "$(ProjectDir)" @(VersionNumber) The BUILDAs we have now the BuildLogs, we can use them in the template: 123456789101112131415161718192021<# ... //Get BuildLog of max version string buildlogFolder = this.Host.ResolvePath("BuildLogs"); string buildLog = buildlogFolder + "\\\\" + maxVersion.major + "." + maxVersion.minor + ".log"; //Get number of lines from BuildLog or create a new log (!) var buildCount = 1; if (File.Exists(buildLog)) { buildCount = File.ReadLines(buildLog).Count() + 1; } else { File.Create(buildLog).Dispose(); } //Set BUILD int build = buildCount;#> Very important is to create the log file, if it doesn’t exists! Otherwise the build will always fail, because the version attributes can’t be created. The REVISIONAt least we have to set the REVISION number, by calculating the difference between the current date and the initialDate, which we have previously read from the AssemblyVersion.json: 1234567<# ... //Set REVISION var dateCreated = DateTime.Parse(avObject.initialDate); int revision = (DateTime.Now.Date - dateCreated.Date).Days;#> Transforming T4 template on buildThe last hurdle is to run the text transformation every time you build your product. Until now it runs only on saving the AssemblyVersion.tt. A great helper on that was Thomas Levesque’s post “Transform T4 templates as part of the build, and pass variables from the project”, where he describes every difficulty to reach this goal. To make it short: We have to edit the CSPROJ file again, to introduce TextTemplating to MSBuild. First we need following near the beginning of the projects XML: 1234567891011<PropertyGroup> <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''"> 16.0 </VisualStudioVersion> <VSToolsPath Condition="'$(VSToolsPath)' == ''"> $(MSBuildExtensionsPath32)\\Microsoft\\VisualStudio\\v$(VisualStudioVersion) </VSToolsPath> <TransformOnBuild>true</TransformOnBuild> <OverwriteReadOnlyOutputFiles>true</OverwriteReadOnlyOutputFiles> <TransformOutOfDateOnly>false</TransformOutOfDateOnly></PropertyGroup> Secondly add the IMPORT of the TextTemplating target AFTER the CSharp target: 123<Import Project="$(MSBuildToolsPath)\\Microsoft.CSharp.targets" />...<Import Project="$(VSToolsPath)\\TextTemplating\\Microsoft.TextTemplating.targets" /> If you build your product now, a new build log is created and the version numbers BUILD and REVISION are automatically increased. See it in actionThe project where I implemented this versioning first is HexoCommander. Feel free to download the code and see how the new versioning mechanism works. Enjoy versioning…","categories":[{"name":".NET","slug":"NET","permalink":"https://kiko.io/categories/NET/"}],"tags":[{"name":"Visual Studio","slug":"Visual-Studio","permalink":"https://kiko.io/tags/Visual-Studio/"},{"name":"Versioning","slug":"Versioning","permalink":"https://kiko.io/tags/Versioning/"},{"name":"T4","slug":"T4","permalink":"https://kiko.io/tags/T4/"}]},{"title":"Automatic Header Images in Hexo","subtitle":"Use static images randomly for posts via Hexo script","series":"A New Blog","date":"2020-06-22","updated":"2020-06-22","path":"post/Automatic-Header-Images-in-Hexo/","permalink":"https://kiko.io/post/Automatic-Header-Images-in-Hexo/","excerpt":"Every article in this blog has an individual header image, to bring a little bit color into it. In this post I will show you how I deal with the images in using and automatic provisioning. For serving these pictures I use a static folder, as described in A New Blog: Customizing Hexo. The hard work is done by the plugin Hexo Generator Copy, which copies the static files into the public_dir while generating.","keywords":"article blog individual header image bring bit color post show deal images automatic provisioning serving pictures static folder customizing hexo hard work plugin generator copy copies files public_dir generating","text":"Every article in this blog has an individual header image, to bring a little bit color into it. In this post I will show you how I deal with the images in using and automatic provisioning. For serving these pictures I use a static folder, as described in A New Blog: Customizing Hexo. The hard work is done by the plugin Hexo Generator Copy, which copies the static files into the public_dir while generating. Static File StructureIt is always advisable to provide one image for every device class, in order to save bandwidth and make the page loading as fast as possible: 1234567891011| static/ | photos/ | mobile/ | my-lovely-picture.jpg | ... | tablet/ | my-lovely-picture.jpg | ... | normal/ | my-lovely-picture.jpg | ... The mobile images are at least 480 pixels wide, the tablet variants 768 pixels and the standard or normal one 1280 pixels. While creating the JPG files, it is important to compress them with a tool like JPEGMini to save data while loading. BindingIn order to bind a picture with some additional information to an article, I have extended the Frontmatter of every post: 1234photograph: file: 'my-lovely-image.jpg' name: 'My Lovely Image' link: 'https://500px.com/photo/123456789/My-Lovely-Image' Usage in ThemeIt relies on your Hexo theme, how to use a header image. In my theme (derived from the standard theme) I just added following code in the article.js to show the individual header image as a background image at the top of the article: 123456789101112131415161718192021222324252627<% if (!index && post.photograph){ %><style> #banner { background-size: cover; } @media screen and (max-width: 479px) { #banner { background-image: linear-gradient(to bottom, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 75%), url("/photos/mobile/<%= post.photograph.file %>"); } } @media screen and (min-width: 480px) and (max-width: 767px) { #banner { background-image: linear-gradient(to bottom, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 75%), url("/photos/tablet/<%= post.photograph.file %>"); } } @media screen and (min-width: 768px) { #banner { background-image: linear-gradient(to bottom, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 75%), url("/photos/normal/<%= post.photograph.file %>"); } }</style><script> var photoLink = document.getElementById("header-photo-link"); photoLink.href = "<%= post.photograph.link%>"; photoLink.innerHTML = "see <strong><%= post.photograph.name%></strong> at 500px";</script><% } %> Important part here is the use of the Frontmatter data post.photograph.file in the URL of the background CSS. The script fills the additional information into the absolute positioned element header-photo-link which is placed on top of the header. Pooling ImagesAs it is time consuming to generate the necessary images, I have created another static folder pool to store prepared files and a text file with the additional information about the image. The structure of pool is different to photos, because of my image workflow and some limitations of automating the provisioning. 12345678| static/ | pool/ | my-lovely-picture/ | meta.txt | mobile.jpg | normal.jpg | tablet.jpg | ... The meta.txt is a simple text file with two lines of text: first the name of the image and second the Url to link to, which will be inserted in the appropriate Frontmatter fields on creating a new post: 12My Lovely Imagehttps://500px.com/photo/123456789/My-Lovely-Image Automate binding and provisioning on new postDevelopers are lazy and I do not make an exception. Having all these pool images and the meta informations, it would be nice, if Hexo just picks and processes one of the pool folders automatically, when I’m creating a new post by calling hexo new "My shiny new post" … and it was easier then I thought. Where to place the code for the automatismHexo has a great API to write plugins and it is not very difficult to setup a new plugin for this, which can be published to the NPM registry. But it is also possible to extend Hexo’s functionality by using a simple script. All you need is a script folder in the root of your Hexo project. Any JS files which is placed there, will be executed by Hexo. Therefore, lets use a script called \\scripts\\process-photo-on-new.js … Things an automatism should do - Step by Step Hook into the creation of a post Pick randomly one of the pool images Place the content of the meta.txt in the Frontmatter Move the 3 device-dependend images into the photos folder Step 1 - Hook into the creation of a postThe needed event, the automatism can hook on, is: 123hexo.on('new', function(data){ //}); It will be executed every time you call the hexo new command. The parameter data is an object with two fields: pathFull path to the MD file of the new post contentComplete content of the scaffold (template), which Hexo has used to create the new post; default is /scaffolds/post.md. By preloading the Hexo Front matter library and parsing data.content we get access to the definition of the new post: 1234567const front = require('hexo-front-matter');hexo.on('new', function(post){ // parse article content var post = front.parse(data.content);}); Step 2 - Pick randomly one of the pool imagesThere are some build-in variables to get the full path, for example, of the source folder, we can use to define the needed paths to the pool and the photo folder. 123456789const front = require('hexo-front-matter');hexo.on('new', function(post){ var post = front.parse(data.content); // set the path variables var poolDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "pool"; var photosDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "photos";}); Next, we need to preload the Hexo FS library for file access, to list the content of the poolDir, including the subfolders, and filter out the meta files. Out of the resulting array we pick one randomly, to use for the new post: 123456789101112131415161718192021const front = require('hexo-front-matter');const fs = require('hexo-fs');hexo.on('new', function(post){ var post = front.parse(data.content); var poolDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "pool"; var photosDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "photos"; // list all files var files = fs.listDirSync(poolDir); // filter the list to get meta files of each subfolder var metaFiles = files.filter(file => file.match(/.*[\\\\]meta.txt/g)); // pick one randomly var metaFile = metaFiles[Math.floor(Math.random() * metaFiles.length)]; // get the name of the picked photo (foldername) var photoName = metaFile.split("\\\\")[0];}); Step 3 - Place the content of the meta.txt in the FrontmatterNow we have to read the meta file, place the data in the Frontmatter and save the article file: 123456789101112131415161718192021222324252627282930const front = require('hexo-front-matter');const fs = require('hexo-fs');hexo.on('new', function(post){ var post = front.parse(data.content); var poolDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "pool"; var photosDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "photos"; var files = fs.listDirSync(poolDir); var metaFiles = files.filter(file => file.match(/.*[\\\\]meta.txt/g)); var metaFile = metaFiles[Math.floor(Math.random() * metaFiles.length)]; var photoName = metaFile.split("\\\\")[0]; // read meta file var meta = fs.readFileSync(poolDir + "\\\\" + metaFile); var metas = meta.split("\\n"); // place file and additional info in the Frontmatter post.photograph.file = photoName + ".jpg"; post.photograph.name = metas[0]; post.photograph.link = metas[1]; // convert content back postStr = front.stringify(post); postStr = '---\\n' + postStr; // store article fs.writeFile(data.path, postStr, 'utf-8');}); Step 4 - Move the 3 device-dependend images into the photos folderLast but not least, we have to move the pool images into the photos folder and remove the pool folder with all its processed content: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051const front = require('hexo-front-matter');const fs = require('hexo-fs');hexo.on('new', function(post){ var post = front.parse(data.content); var poolDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "pool"; var photosDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "photos"; var files = fs.listDirSync(poolDir); var metaFiles = files.filter(file => file.match(/.*[\\\\]meta.txt/g)); var metaFile = metaFiles[Math.floor(Math.random() * metaFiles.length)]; var photoName = metaFile.split("\\\\")[0]; var meta = fs.readFileSync(poolDir + "\\\\" + metaFile); var metas = meta.split("\\n"); post.photograph.file = photoName + ".jpg"; post.photograph.name = metas[0]; post.photograph.link = metas[1]; postStr = front.stringify(post); postStr = '---\\n' + postStr; fs.writeFile(data.path, postStr, 'utf-8'); //copy normal image fs.copyFile( poolDir + "\\\\" + photoName + "\\\\normal.jpg", photosDir + "\\\\normal\\\\" + photoName + ".jpg", function() { //copy tablet image fs.copyFile( poolDir + "\\\\" + photoName + "\\\\tablet.jpg", photosDir + "\\\\tablet\\\\" + photoName + ".jpg", function() { //copy mobile image fs.copyFile( poolDir + "\\\\" + photoName + "\\\\mobile.jpg", photosDir + "\\\\mobile\\\\" + photoName + ".jpg", function() { //remove processed pool folder fs.rmdirSync(poolDir + "\\\\" + photoName); }); }); });}); Now it so easy to write a new post, because almost everything is set and I can concentrate on the article. Also, it is a nice surprise to see, which photo the script has chosen. The only thing I have to do from time to time, is to refill the pool folder with new images.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"Localization with resource files in JavaScript web apps","subtitle":"How to work with Visual Studio resource files for localization in Single Page Applications","date":"2020-06-13","updated":"2020-06-13","path":"post/Localization-with-resource-files-in-JavaScript-web-apps/","permalink":"https://kiko.io/post/Localization-with-resource-files-in-JavaScript-web-apps/","excerpt":"There are plenty of editors out there to help you writing JavaScript web applications. As I’m working in my daily life with Visual Studio, it is a obvious choice for me. One of the most time saving tools in VS is the plugin ResXManager, which is an awesome assistant on managing the translations for a Desktop- or ASP.NET-App, which uses XML-based RESX files.","keywords":"plenty editors writing javascript web applications im working daily life visual studio obvious choice time saving tools plugin resxmanager awesome assistant managing translations desktop- aspnet-app xml-based resx files","text":"There are plenty of editors out there to help you writing JavaScript web applications. As I’m working in my daily life with Visual Studio, it is a obvious choice for me. One of the most time saving tools in VS is the plugin ResXManager, which is an awesome assistant on managing the translations for a Desktop- or ASP.NET-App, which uses XML-based RESX files. Mostly very localization is based on key/value pairs, defined in separate files for every language provided. Implementing several languages in pure JavaScript apps is a little more difficult, because it makes no sense to deal with big XML files in JS. All localization libraries in the market uses JSON for storing the translations and it is a little bit of work to find the right one for your requirements. Localization in JavaScriptFor a current project I use jquery-lang, because it provides the switch of the apps UI language without reloading and it is easy to implement. Thanks Rob Evans for your work… The definition of “tokens” in one JSON file for each language is quite easy: ../languages/en.json12345{ "token": { "my-test": "My Test in English" }} ../languages/de.json12345{ "token": { "my-test": "Mein Test in Deutsch" }} The usage also: 1<div lang="en" data-lang-token="my-test"> Using RESX and convert to JSON on buildHaving this, the most time consuming work is to enter the translations to the localization files. If you have hundreds of them, it is hard to keep the 2, 3 or more language files in sync. You need a helper… And here comes ResXManager to the rescue, if you work with VS … but it needs a conversation from RESX to the JSON format jquery-lang uses and this a task, which can be done on building the JS app, by using a task runner like Grunt. As there was no Grunt plugin/task out there to fit my needs, I have created grunt-resource2json (GitHub, NPM). The configuration in the gruntfile.js is like: gruntfile.js12345678910111213141516171819202122grunt.initConfig({ resource2json: { convert: { options: { format: "jquery-lang" }, files: [ { input: "resources/Resource.resx", output: "build/langpacks/en.json" }, { input: "resources/Resource.de-DE.resx", output: "build/langpacks/de.json" }, { input: "resources/Resource.es-ES.resx", output: "build/langpacks/es.json" } ] } }); It takes one RESX file (input) and converts it to a JSON file (output) in an array of files. The heavy work in the plugin is done by the library xml2js, which transforms the complete XML of the RESX file into a JSON object in one call. All I had to do, was to write all DATA nodes in a loop into the jquery-lang given structure and save it as JSON. Currently supported is the format for jquery-lang only, but it would be awesome, if you fork the code on GitHub and send me a Pull Request with the implementation of your needed format.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Visual Studio","slug":"Visual-Studio","permalink":"https://kiko.io/tags/Visual-Studio/"},{"name":"Resource","slug":"Resource","permalink":"https://kiko.io/tags/Resource/"},{"name":"Localization","slug":"Localization","permalink":"https://kiko.io/tags/Localization/"}]},{"title":"TFS/DevOps: Delete Remote Workspace","subtitle":null,"date":"2020-02-27","updated":"2020-02-27","path":"post/TFS-DevOps-Delete-Remote-Workspace/","permalink":"https://kiko.io/post/TFS-DevOps-Delete-Remote-Workspace/","excerpt":"If you are working with freelance developers and Azure DevOps/TFS with TFVC (Team Foundation Version Control) in your company, maybe this will look familiar to you: You hire a new freelancer and you want to reuse the hardware, including the complete software setup, to bring him/her to work as fast and straightforward as possible. You set up a new Azure Devops account with all necessary permissions and you think you’re done. No you are not…","keywords":"working freelance developers azure devops/tfs tfvc team foundation version control company familiar hire freelancer reuse hardware including complete software setup bring him/her work fast straightforward set devops account permissions youre not…","text":"If you are working with freelance developers and Azure DevOps/TFS with TFVC (Team Foundation Version Control) in your company, maybe this will look familiar to you: You hire a new freelancer and you want to reuse the hardware, including the complete software setup, to bring him/her to work as fast and straightforward as possible. You set up a new Azure Devops account with all necessary permissions and you think you’re done. No you are not… Everytime a user connects to a Team Project on Azure DevOps via Visual Studio and gets the code, VS is creating a remote workspace on the server, with the machine name as default, therefor it is not enough to wipe the profile and any other legacies of the last user from the machine. You also have to remove the remote workspace. Otherwise you will get an error message like that, if you are using a unique file structure on the developers hard disc: 1The working folder c://xxx is already in use by the workspace yyy;zzz on computer yyy The variable xxx stands for the blocked folder, yyy for the workspace/machine name and zzz for the users id on Azure DevOps. Unfortunately, there is no visual management console on Azure DevOps to manage your server workspaces, but there is a command line tool called tf.exe. The easiest way to get rid of the unused server workspace in 3 steps: Step 1Run Developer Command Prompt with Administrator privileges from Visual Studio 2019 and login with your Azure DevOps credentials. If the Login dialog doesn’t show up, force it by executing: 1tf.exe workspace Step 2Get a list of all remote workspaces available in your DevOps Collection by running the command: 1tf.exe workspaces /computer:* /owner:* /format:xml > c:\\temp\\workspaces.xml You can get a list of all your workspaces by running tf workspaces, but the list only shows you the owner, but not the necessary ownerid and … it is nicer to have a file to search in. Step 3Find the abandoned workspace in the list and note its name and ownerid for running the command: 1tf workspace /delete {WORKSPACE.name};{WORKSPACE.ownerid} Now your new colleague can create his own workspace on the same machine. UpdateIn case you want to switch your own DevOps account to another and use the same folder as before, you can certainly delete the local workspace, but this wont help, because you are still logged in at TeamExplorer and the folder knows to whom it belongs. Solution is easy: Quit Visual Studio Rename folder in ***_OLD or something Create new folder with the same name Enter C:\\Users\\YOUR-NAME\\appdata\\Local\\Microsoft\\Team Foundation\\VS-VERSION\\Cache and emtpy the folder to let Visual Studio forget who you are Remove all your Remote Workspaces as described above Start Visual Studio, connect in TeamExplorer to your TFS server and map the code to your folder More Info Microsoft Docs: Use Team Foundation version control commandsStackoverflow: How to remove TFS workspace mapping for another user","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Visual Studio","slug":"Visual-Studio","permalink":"https://kiko.io/tags/Visual-Studio/"},{"name":"TFS/DevOps","slug":"TFS-DevOps","permalink":"https://kiko.io/tags/TFS-DevOps/"}]},{"title":"Better Input Change Event","subtitle":null,"date":"2019-11-26","updated":"2019-11-26","path":"post/Better-Input-Change-Event/","permalink":"https://kiko.io/post/Better-Input-Change-Event/","excerpt":"Often it is important to trigger an event, after the user of your website/web app has filled out an text input. You have to do something with the given value in JavaScript. The intended event for this is change, which will be triggered, when the user has ended changing by leaving the input with his cursor, mostly by using the TAB key. This works at some degree, if there is a physical keyboard, but not really on mobile devices … and for me is leaving the field often too late to start the upcoming event.","keywords":"important trigger event user website/web app filled text input javascript intended change triggered ended changing leaving cursor tab key works degree physical keyboard mobile devices … field late start upcoming","text":"Often it is important to trigger an event, after the user of your website/web app has filled out an text input. You have to do something with the given value in JavaScript. The intended event for this is change, which will be triggered, when the user has ended changing by leaving the input with his cursor, mostly by using the TAB key. This works at some degree, if there is a physical keyboard, but not really on mobile devices … and for me is leaving the field often too late to start the upcoming event. A better way to show the user the result of his entered value, could be the event input which fires on every key stroke, but could be way to often, if the triggered event is for example an AJAX call. Best solution is, to observe the users key strokes and trigger the event, when he stops typing. Then there is no extra action needed by the user and the event isn’t triggered multiple times. Here’s an implementation with jQuery: 12345678910$("#my-text-input").keyup(function () { var $this = $(this); clearTimeout($.data(this, 'timer')); var wait = setTimeout(function () { //do something with the value... }, 1000); $(this).data('timer', wait);}); Important is to wipe and set the timer on every key up, to achive that the event will be executed after 1 second after the last key stroke only.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"jQuery","slug":"jQuery","permalink":"https://kiko.io/tags/jQuery/"}]},{"title":"Hexo and the Dark Mode ... revised","subtitle":"Second approach to implement 'prefers-color-scheme'","series":"A New Blog","date":"2019-10-26","updated":"2019-10-26","path":"post/Hexo-and-the-Dark-Mode-revised/","permalink":"https://kiko.io/post/Hexo-and-the-Dark-Mode-revised/","excerpt":"While writing my post Hexo and the Dark Mode a few days ago, I thought it would be nice, if I could switch between the normal (light) and the dark theme, I’ve created for the support of the OS-related Dark Mode, even manually. The only thing I needed was a toggle element and a little bit of JavaScript. Of course, I couldn’t manipulate the media query prefers-color-scheme itself, but introduce a different way by blog uses it. Instead of implementing the media query directly into my CSS (or Stylus) code, I used a root selector, which can be manipulated by JavaScript … something like this: 12345678910body { background-color: white; color: black;}[data-theme="dark"] body { background-color: black; color: white; }}","keywords":"writing post hexo dark mode days ago thought nice switch normal light theme ive created support os-related manually thing needed toggle element bit javascript couldnt manipulate media query prefers-color-scheme introduce blog implementing directly css stylus code root selector manipulated … 12345678910body { background-color white color black}[data-theme="dark"] body black }}","text":"While writing my post Hexo and the Dark Mode a few days ago, I thought it would be nice, if I could switch between the normal (light) and the dark theme, I’ve created for the support of the OS-related Dark Mode, even manually. The only thing I needed was a toggle element and a little bit of JavaScript. Of course, I couldn’t manipulate the media query prefers-color-scheme itself, but introduce a different way by blog uses it. Instead of implementing the media query directly into my CSS (or Stylus) code, I used a root selector, which can be manipulated by JavaScript … something like this: 12345678910body { background-color: white; color: black;}[data-theme="dark"] body { background-color: black; color: white; }} In every Stylus file, where I used @media prefers-dark to achieve the automatic switch by the OS, I changed this line into /[data-theme="dark"] & : 12345678#mobile-nav-header background-color: color-background /[data-theme="dark"] & background-color: dark-color-background img.avatar ... /[data-theme="dark"] & filter: brightness(85%) Some explanations on the Stylus syntax: / means the root of the DOM and & points to the parent selector. Therefore the example will be rendered into this: 12345678910111213#mobile-nav-header { background-color: #f1f1f1;}[data-theme="dark"] #mobile-nav-header { background-color: #111;}#mobile-nav-header img.avatar {...}[data-theme="dark"] #mobile-nav-header img.avatar filter: brightness(85%);} Only problem was: the “Root + Parent” Stylus selector doesn’t work in the block variables in the _extend.styl. So I had to copy all theme relevant styles directly to the elements, where such a block was used: @extend <block-name>. The Toggle SwitchIn the footer.ejs I added a toggle checkbox, where I could bind my JavaScript… 1234<div id="footer-theme"> <input type="checkbox" id="theme-switch"> <label for="theme-switch"></label></div> … and some CSS in the footer.styl, to style it: 12345678910111213141516171819202122input#theme-switch[type=checkbox] { display:none;}input#theme-switch[type=checkbox] + label height: 16px width: 16px display: inline-block padding: 12px font-size: 22px cursor: pointer &:before display: inline-block font-size: inherit text-rendering: auto -webkit-font-smoothing: antialiased font-family: fa-icon-solid content: icon-mooninput#theme-switch[type=checkbox]:checked + label &:before content: icon-sun The icon variables are defined in the _variables.styl like this: 12icon-moon = "\\f186"icon-sun = "\\f185" The JavaScriptEverything was now prepared to implement the switching code in JavaScript, which should support a manual switch by clicking the toggle element as well as the automatic switch by the OS. I wrapped all necessary code into a seperate JS file and placed a reference in the after-footer.ejs, which places it at the bottom of the HTML: 1<%- js('js/dark-mode-toggle.js') %> 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758function detectColorScheme() { var theme = "light"; //default // get last used theme from local cache if(localStorage.getItem("theme")){ if(localStorage.getItem("theme") === "dark"){ theme = "dark"; } } else if(!window.matchMedia) { // matchMedia not supported return false; } else if(window.matchMedia("(prefers-color-scheme: dark)").matches) { // OS has set Dark Mode theme = "dark"; } // set detected theme if (theme === "dark") { setThemeDark(); } else { setThemeLight(); }}const toggleTheme = document.querySelector('input#theme-switch[type="checkbox"]');function setThemeDark() { localStorage.setItem('theme', 'dark'); document.documentElement.setAttribute('data-theme', 'dark'); toggleTheme.checked = true;}function setThemeLight() { localStorage.setItem('theme', 'light'); document.documentElement.setAttribute('data-theme', 'light'); toggleTheme.checked = false;}// Listener for theme change by toggletoggleTheme.addEventListener('change', function(e) { if (e.target.checked) { setThemeDark(); } else { setThemeLight(); }}, false);// Listener for theme change by OSvar toggleOS = window.matchMedia('(prefers-color-scheme: dark)');toggleOS.addEventListener('change', function (e) { if (e.matches) { setThemeDark(); } else { setThemeLight(); }});// call theme detectiondetectColorScheme(); By using the both addEventListener‘s, each switch will be recognized and this approach is capable to support even more themes, just by using different values in the data-theme attribute.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Stylus","slug":"Stylus","permalink":"https://kiko.io/tags/Stylus/"},{"name":"Dark Mode","slug":"Dark-Mode","permalink":"https://kiko.io/tags/Dark-Mode/"}]},{"title":"Hexo and the Dark Mode","subtitle":"First approach to implement 'prefers-color-scheme'","series":"A New Blog","date":"2019-10-23","updated":"2019-10-23","path":"post/Hexo-and-the-Dark-Mode/","permalink":"https://kiko.io/post/Hexo-and-the-Dark-Mode/","excerpt":"Due to the fact, that nowadays everybody is talking about Dark Modes for Browsers and Operating Systems, in order to save battery or for easier reading (uhh, really?), I decided my blog should support that.","keywords":"due fact nowadays talking dark modes browsers operating systems order save battery easier reading uhh decided blog support","text":"Due to the fact, that nowadays everybody is talking about Dark Modes for Browsers and Operating Systems, in order to save battery or for easier reading (uhh, really?), I decided my blog should support that. Starting point is the new media query prefers-color-scheme, which is actually supported by all modern browsers. TechniqueMy first read was Tom Brow’s Dark mode in a website with CSS, where he shows how to use the media query. Simplified, this is it, assuming the light version is the default: 1234567891011body { background-color: white; color: black;}@media (prefers-color-scheme: dark) { body { background-color: black; color: white; }} Pimping CSS for automatic switchingTo support the automatic browser/OS-based automatic switch in Hexo, where Stylus is used, I had to change some template files. First the _variables.styl: 1234567891011121314// existing color variablescolor-background = #f1f1f1color-foreground = #111color-border = #ddd...// new dark color variablesdark-color-background = #111dark-color-foreground = #eeedark-color-border = #000...// new media query variableprefers-dark = "(prefers-color-scheme: dark)" Next step was to change the _extend.styl, where some Stylus variables are defining complete blocks to extend. Here I had to supplement all lines, where something mode-dependend was defined, by adding the new prefers-dark media query and beneath the new ‘dark’ equivalence of the style: 12345678910111213141516171819$base-style hr ... border: 1px dashed color-border-article @media prefers-dark border: 1px dashed dark-color-border-article ...$block ... background: color-block box-shadow: 1px 2px 3px color-border border: 1px solid color-border @media prefers-dark background: dark-color-block box-shadow: 1px 2px 3px dark-color-border border-color: dark-color-border... The same changes I had to do in every template styl file, where one of the colors or other mode dependent style was used. For example: 12345678#mobile-nav-header background-color: color-background @media prefers-dark background-color: dark-color-background img.avatar ... @media prefers-dark filter: brightness(85%) This will be rendered as: 123456789101112131415#mobile-nav-header { background-color: #f1f1f1;}@media (prefers-color-scheme: dark) { #mobile-nav-header { background-color: #111; }}#mobile-nav-header img.avatar { ...}@media (prefers-color-scheme: dark) { filter: brightness(85%);} Please note the use of filter:brightness() in the example. It is always advisable to darken the images too, because they can really pop out on dark backgrounds.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Stylus","slug":"Stylus","permalink":"https://kiko.io/tags/Stylus/"},{"name":"Dark Mode","slug":"Dark-Mode","permalink":"https://kiko.io/tags/Dark-Mode/"}]},{"title":"A New Blog: Blogging and Synching en route","subtitle":"Part Three of having fun with Hexo and GitHub Pages","series":"A New Blog","date":"2019-09-30","updated":"2019-09-30","path":"post/A-New-Blog-Blogging-and-Synching-en-route/","permalink":"https://kiko.io/post/A-New-Blog-Blogging-and-Synching-en-route/","excerpt":"I work with several devices, some Windows, some Android, and sometimes I have time to write on my articles at home (Notebook, Tablet), in my spare time in the office (Desktop, Laptop) or on my way to somewhere (Smartphone). Right now I’m am in a barber shop, waiting for my haircut and write these lines. So, wherever I am, I need the Hexo project locally, but in sync on a digital device. The blog is synced via Dropbox, but hosted on GitHub Pages, so on every device I need the publishing functions of Git too.","keywords":"work devices windows android time write articles home notebook tablet spare office desktop laptop smartphone im barber shop waiting haircut lines hexo project locally sync digital device blog synced dropbox hosted github pages publishing functions git","text":"I work with several devices, some Windows, some Android, and sometimes I have time to write on my articles at home (Notebook, Tablet), in my spare time in the office (Desktop, Laptop) or on my way to somewhere (Smartphone). Right now I’m am in a barber shop, waiting for my haircut and write these lines. So, wherever I am, I need the Hexo project locally, but in sync on a digital device. The blog is synced via Dropbox, but hosted on GitHub Pages, so on every device I need the publishing functions of Git too. Sync Hexo ProjectBest option for me to achieve this was Dropbox. Another benefit on that is: I can work on the structure of the blog wherever I am and commit when the new feature or improvement is done, because all Git related files are always in sync too. Writing, Editing and Publishing on WindowsMy preferred editor is Visual Studio Code. Good file handling, easy writing, full Git integration and tons of other plugins and helpers. Chapeau Microsoft, well done. Some of the following VS Code plugins makes working with Hexo on GitHub pages a breeze: Adds Hexo commands like init, new, generate, server and clean to the VS Code command palette. Keyboard shortcuts for basic formatting, automatic list editing, autocomlete for images, table formatter and much more for an easier handling of Markdown. Markdown linting and style checking Adds syntax highlighting and code completion to Stylus files Complete visual management of your repositories in VS Code View a Git Graph of your repository with all changes and manage commits. With this editor and its helpers, I’m just two clicks away from publishing a new article or even a new version of the Hexo blog itself. Writing on AndroidThere are a lot of Markdown editors available on Google Play, but one is outstanding: iA Writer for Android. I can open my posts or drafts directly from Dropbox, without the need of any sychronization. Open, write, close, done. Publishing on AndroidThere are some Git related Android apps out there, but no solution was satisfying. Furthermore, I didn’t really need Git here, because I didn’t want to have all source files on my smartphone. I’m working directly on the Dropbox stored MD files via iA Writer. Finally and most important, Git won’t be enough, because before publishing, I have to run hexo generate! Therefore some sort of automatic transfer from Dropbox to GitHub is also out of the game. What I needed, was to tell a server at a certain point of time ‘Hey, please publish for me’, using the only connection I have: Dropbox. Introducing a DemonI have a little media server, running on Windows, and he is synchronizing some folders with Dropbox. He could do the job! After I installed all necessary packages, like NodeJS, Hexo and Git, I included the project folder into the sync. Next step was to design a so called Hexo Command File, a simple TXT file, which holds commands in single lines, extended with execution times, when they were successfully running. 12345postdraft: A-New-Blog-Blogging-and-Synching-en-routepublishnewdraft: "A New Blog: Blogging and Synching en route" @ 2019-09-30 21:15regenerate @ 2019-09-29 16:40:01publish @ 2019-09-29 16:40:10 These commands are predefined, because they bundle several real commands and I didn’t want to deal with real commands, due to security reasons. The unprocessed commands are standing at the top of the file (in execution order!) and parameters are separated from the command by a colon and delimited by commas. <command>: [<param1>, ...] @ <execution time> Next step was to create a program to work as an executing demon, who monitors the Hexo Command File (synced by Dropbox) on my server and executes commands without execution dates. I decided to create a simple Console Application in C# and use the built-in Windows Task Scheduler for running it every 2 minutes. The application is called HexoCommander and is available at GitHub. It expects the Hexo Command File to be named hexo-commands.txt, located in the same folder, and provides the following commands: newdraft: “<title>” … runs hexo new draft "<title>" Creates a new draft. postdraft: “<filename without extension>” … runs hexo publish "<filename without extension>" Makes a post out of a draft. regenerate … runs hexo clean hexo generate Wipes all Hexo static pages and generates them new. publish … runs hexo generate git add "source/*" "docs/*" git commit -m "Remote publication via HexoCommander" git push origin master Generates Hexo static pages, stage changes on drafts, posts and static pages, commits the changes with a generic message and pushes them to the server. Running the demonI would have never expected, that the trickiest part was to get HexoCommander running via Windows Task Scheduler. What a mess! I finally find the solution here: Compile HexoCommander in a x86 configuration Create a new task in Task Scheduler with Trigger Dialy Recur every 1 days Repeat task every 2 minutes for a duration of 1 day Action Program/Script: %systemroot%\\Syswow64\\cmd.exe Add Arguments: /C “C:\\MyPath\\HexoCommander.exe /workdir=C:\\MyPath” Start In: %systemroot%\\Syswow64\\ Because some executing commands in the chain are NOT 64-bit, I had to force Task Scheduler to run the 32-bit Command Shell in its own path (see ‘Start In’ and don’t forget the closing backslash) and take the 32-bit compiled HexoCommander as argument after the parameter /C (forcing command to terminate), including its own argument for defining the real working directory. Mind bending, but works…","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"A New Blog: Customizing Hexo","subtitle":"Part Two of having fun with Hexo and GitHub Pages","series":"A New Blog","date":"2019-09-25","updated":"2019-09-25","path":"post/A-New-Blog-Customizing-Hexo/","permalink":"https://kiko.io/post/A-New-Blog-Customizing-Hexo/","excerpt":"Hexo is a great tool to get quick results (see Part One of this series), when you decide to have a blog and its defaults are practical, but it’s power lies in the possiblities of customization via plugins. On the official plugin page, there are actually 302 plugins listed, but there are many more and no wish will be unsatisfied. I will show you which of these I found worth to work with…","keywords":"hexo great tool quick results part series decide blog defaults practical power lies possiblities customization plugins official plugin page listed unsatisfied show found worth work with…","text":"Hexo is a great tool to get quick results (see Part One of this series), when you decide to have a blog and its defaults are practical, but it’s power lies in the possiblities of customization via plugins. On the official plugin page, there are actually 302 plugins listed, but there are many more and no wish will be unsatisfied. I will show you which of these I found worth to work with… Relative Image PathThe build-in way to include images in your posts works fine, but it is a little aside the normal way to declare images in Markdown. The plugin [Hexo Asset Link] corrects that. After installing via npm install hexo-asset-link --save you can write this: ![Test Image](hello-world/image-1.png) The best is, that VS Code’s Markdown can now show the image. UPDATE:Actually the plugin destroys external links, so don’t use it until this is fixed … or go to node_modules > hexo-asset-link > index.js in your project and change in line 22 protocal to protocol. UPDATE from Update:liolok, the author of the plugin has merged my pull request and published a new new version without the typo. It works now as expected. Hide PostsA new Hexo project comes with a sample post called Hello World. This is fine to play around with, but you don’t want to publish it. Here comes a Hexo plugin to the rescue called Hexo Hide Posts. After installing, you just have to write hidden: true to the Front Matter of you post and it won’t be shown on the blog, but it is still available by URL. Static FilesHexo has the concept of Assets Folders, but for static files, beside article based files, I find it more useful to have a STATIC folder and copy the contents on every build into the publish folder. A good helper for this approach is the plugin Hexo Generator Copy. Install it by running npm install hexo-generator-copy --save and add static_dir: static to your _config.yml and you are done. ![Hexo Static Files](/post/A-New-Blog-Customizing-Hexo/vscode-1.png) FeedThe default Hexo layout has an Atom Feed icon in the upper right corner, but strangely no feed file is generated on build. You need to install the plugin Hexo Feed Generator to fix this, by running npm install hexo-generator-feed --save and copy following section into the _config.yml: 123456789feed: type: atom path: atom.xml limit: 20 hub: content: content_limit: 140 content_limit_delim: ' ' order_by: -date Manifest for PWAIn these modern times it’s always a good idea, that your blog feels like an App. For this you need a manifest file (JSON) an several icons (PNG). You can generate these files very fast with the Web App Manifest Generator and store it in your static folder. To bind this file into your blog, you can use the plugin Hexo PWA. Run npm install --save hexo-pwa and copy following section to your _config.yml, where you take the settings from your generated manifest file: 1234567891011121314151617pwa: manifest: path: /manifest.json body: name: myblog.de short_name: My Blog icons: - src: /images/icon-192x192.png sizes: 192x192 type: image/png - src: /images/icon-512x512.png sizes: 512x512 type: image/png start_url: /index.html theme_color: '#025ab1' background_color: '#dddddd' display: standalone Sitemap FileTo help Google and others a bit to index your blog, it is advisable to provide a sitemap file. Here comes Hexo Generator Sitemap to the rescue. Install it by running the command npm install hexo-generator-sitemap --save. You can configure it via _config.yml: 123sitemap: path: sitemap.xml template: ./sitemap-template.xml The plugin installation doesn’t create the needed sitemap-template file, so be sure you grab a copy from the plugins repository: https://github.com/hexojs/hexo-generator-sitemap/blob/master/sitemap.xml CommentingHexo doesn’t have a commenting system, but it’s prepared to stick Disqus comments under each article. Just create a new Disqus account for your blog and note the given short name. By adding following section to the _config.yml Hexo shows the commenting section: 12disqus_enabled: truedisqus_shortname: my-blog Inifinite ScrollHexo shows as much articles at the start page as configured in _config.yml under index_generator.per_page, but it’s nicer to load more articles as you scroll by using the Hexo script Inifinite Scroll. Install by adding following little script in themes & gt; layout > _partial > after-footer.ejs 12345678<script src="//cdn.jsdelivr.net/gh/frontendsophie/hexo-infinite-scroll@2.0.0/dist/main.js"></script> <script> infiniteScroll({ showNum: 5, style: 'line-scale', color: '#025ab1' })</script> Back To TopIts nice to support the reader on scolling by providing a Scroll-To-Top button. The easiest way to get this, is the script Vanilla Back To Top. Just add follwing to themes >layout > _partial > after-footer.ejs: 123456789101112131415<script>addBackToTop({ diameter: 30, backgroundColor: 'rgb(0, 90, 180)', textColor: '#fff'})</script><style>#back-to-top { border-radius: 0; opacity: 0.6;}#back-to-top:hover { opacity: 1;}</style>","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"A New Blog: VS Code, Hexo and GitHub Pages","subtitle":"Part One of having fun with Hexo and GitHub Pages","series":"A New Blog","date":"2019-09-23","updated":"2019-09-23","path":"post/A-New-Blog-VS-Code-Hexo-and-GitHub-Pages/","permalink":"https://kiko.io/post/A-New-Blog-VS-Code-Hexo-and-GitHub-Pages/","excerpt":"A few days ago I puzzled over a technical problem regarding SQL Server, Active Directory and Visual Studio Database Projects. With tips, hints and snippets from several websites I got it running and the solution was absolutely memorable. For myself and for others. Nothing is harder than to know ‘you did this before…’, but not to remember how. Because of this strong leaning towards oblivion, I started over 20 years ago my very first website zerbit.de, manually crafted with Classic ASP and a SQL Server database as backend, with an editor, tagging, categories and so on. It was really exciting to build this blog from scratch and writing articles for it, but it was so time consuming to expand the features of the website and keep it running, that some day I quit it silently. So, to document the solution mentioned above, I could use tools like OneNote or others, like in the past years, but this would be just for me and not for all developers, who have a similar problem. I felt it would be unfair to participate from the knowledge of others to get my solution and dont give something back. I decided to write an article just in HTML and publish it on my personal GitHub Page that I didn’t used so far. Ok, just Text … ugly. Just a little CSS and a little more structure and maybe I should do something with Vue JS … STOP … It would be better to pick one of the cool new static website generators based on Node.js, to detain myself from inventing the wheel again and save my time to write articles. So I did a little research and found HEXO … Bingo! I can work with my favorite editor Visual Studio Code, its all HTML, JavaScript and CSS, I can write my articles in Markdown and Hexo has a lot of helpers for stuff Markdown doesn’t provide, it produces static files and works only with files, therefore no need for a database … and it is well documented.","keywords":"days ago puzzled technical problem sql server active directory visual studio database projects tips hints snippets websites running solution absolutely memorable harder before… remember strong leaning oblivion started years website zerbitde manually crafted classic asp backend editor tagging categories exciting build blog scratch writing articles time consuming expand features day quit silently document mentioned tools onenote past developers similar felt unfair participate knowledge dont give back decided write article html publish personal github page didnt text … ugly css structure vue js stop pick cool static generators based nodejs detain inventing wheel save research found hexo bingo work favorite code javascript markdown lot helpers stuff doesnt provide produces files works documented","text":"A few days ago I puzzled over a technical problem regarding SQL Server, Active Directory and Visual Studio Database Projects. With tips, hints and snippets from several websites I got it running and the solution was absolutely memorable. For myself and for others. Nothing is harder than to know ‘you did this before…’, but not to remember how. Because of this strong leaning towards oblivion, I started over 20 years ago my very first website zerbit.de, manually crafted with Classic ASP and a SQL Server database as backend, with an editor, tagging, categories and so on. It was really exciting to build this blog from scratch and writing articles for it, but it was so time consuming to expand the features of the website and keep it running, that some day I quit it silently. So, to document the solution mentioned above, I could use tools like OneNote or others, like in the past years, but this would be just for me and not for all developers, who have a similar problem. I felt it would be unfair to participate from the knowledge of others to get my solution and dont give something back. I decided to write an article just in HTML and publish it on my personal GitHub Page that I didn’t used so far. Ok, just Text … ugly. Just a little CSS and a little more structure and maybe I should do something with Vue JS … STOP … It would be better to pick one of the cool new static website generators based on Node.js, to detain myself from inventing the wheel again and save my time to write articles. So I did a little research and found HEXO … Bingo! I can work with my favorite editor Visual Studio Code, its all HTML, JavaScript and CSS, I can write my articles in Markdown and Hexo has a lot of helpers for stuff Markdown doesn’t provide, it produces static files and works only with files, therefore no need for a database … and it is well documented. Installation.. is quite easy, as described here: https://hexo.io/docs/setup Create folder and open in VS Code Open VS Code Terminal window Install Hexo with $ npm install -g hexo-cli Init Hexo project with $ hexo init Install dependencies with npm install Done WritingCreate new post/draftHexo has posts and drafts, whereat drafts has to published via a Hexo command to become a post. To create an article use the command hexo new post|draft "My Title". The title will be converted in a URL-encoded string and will be used as file name and url. Meta dataEvery post/draft starts with its header (so called Front Matter) to store some meta data, which describes the post, like title, date, tags or categories. This is used by Hexo to classify and arrange your post during the build. MarkdownHexo posts/drafts are written in Markdown. Good syntax reference are the Markdown Guide and the more detailed Markdown Syntax Guide. ExcerptIs is usual to show a short excerpt an the start page of a blog, to keep it compact and teasering the user to click on a READ MORE button. To achieve this, you just have to add following comment to your article. Everything above is the excerpt and everything below is only shown, when you enter the article: <!-- more --> ImagesSome articles will contain images to illustrate something and the question is, where should they be stored? Answer: In a folder beside the post/draft, which has the same name as the article MD file. To get this, you have to activate the setting post_asset_folder in your _config.yml. Now this folder will be created automatically, when you add a new post/draft. In your Markdown you reference your image with: {% asset_img image-1.png \"Test Image\" %} BuildHexo is a website generator, so a build will generate the whole website in a special folder, which has to be published. This output folder can be configured in the _config.yml: public_dir: public To wipe the output folder, run the command: hexo clean To start the build, run: hexo generate To view the website via the build-in local Hexo server, run: hexo server PublishingMost “complex” task was to publish the new blog on GitHub Pages. My first approach was to use my personal page, as I did with my single HTML file, but this didn’t work, because I wanted to store the whole project on GitHub and it is not possible to point a personal page to the subdirectory docs or use a different branch as master. The simple solution was to create a new repository, named after my my blog kiko.io, to store teh whole project and point the GitHub Page to the subdirectory docs in the settings of the repository. By overriding the default publish folder of Hexo in _config.yml … public_dir: docs … everything was set up. Commit and Push via git and done. Hexo has its own deploying mechanism and it is advisable to disable it, by commenting out the Deployment section _config.yml. Next step was to use my own custom domain for the blog. To achieve this, the most easiest way is to create a text file named CNAME (without extension!) with the content of the domain in a single line and publish this file in the root of the docs folder. Github will recognize this file and do the setup automatically. To point the domain to GitHub, I had to create following A records in my domain providers DNS settings: 185.199.108.153 185.199.109.153 185.199.110.153 185.199.111.153 Last step was to enable Enforce HTTPS in the repositories settings.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"How-To: Visual Studio Database Project and ADSI","subtitle":null,"date":"2019-09-17","updated":"2019-09-17","path":"post/How-To-Visual-Studio-Database-Project-and-ADSI/","permalink":"https://kiko.io/post/How-To-Visual-Studio-Database-Project-and-ADSI/","excerpt":"If you are working with a Visual Studio Database Project and have to deal with data from the Active Directory via a Linked Server, you have to announce the data structure of the AD data in order to get the project compiled.","keywords":"working visual studio database project deal data active directory linked server announce structure ad order compiled","text":"If you are working with a Visual Studio Database Project and have to deal with data from the Active Directory via a Linked Server, you have to announce the data structure of the AD data in order to get the project compiled. Step 1 - Linking to the Active DirectoryFirst of all you have to connect your SQL Server to the AD permanently, by running following SQL script once on your SQL Server: USE [master] GO EXEC master.dbo.sp_addlinkedserver @server = N'ADSI', @srvproduct=N'Active Directory Service Interfaces', @provider=N'ADSDSOObject', @datasrc=N'adsdatasource' EXEC master.dbo.sp_addlinkedsrvlogin @rmtsrvname=N'ADSI', @useself=N'False', @locallogin=NULL, @rmtuser=N'mydomain\\myadminuser', @rmtpassword='mypassword' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'collation compatible', @optvalue=N'false' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'data access', @optvalue=N'true' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'dist', @optvalue=N'false' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'pub', @optvalue=N'false' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'rpc', @optvalue=N'false' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'rpc out', @optvalue=N'false' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'sub', @optvalue=N'false' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'connect timeout', @optvalue=N'0' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'collation name', @optvalue=null GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'lazy schema validation', @optvalue=N'false' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'query timeout', @optvalue=N'0' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'use remote collation', @optvalue=N'true' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'remote proc transaction promotion', @optvalue=N'true' GO Step 2 - Fetching ADSI dataTo get data, use OpenQuery against the Linked Server. In order to get only persons and no system accounts, you should filter out all users, which has no firstname (givenName) or lastname (sn): SELECT UserPrincipalName, DisplayName, sAMAccountName AS [SamAccountName], sn AS [LastName], givenName AS [FirstName], title AS [Title], Mail as [MailAddress], department AS [Department], l AS [Location], postalCode AS [PostCode], streetAddress AS [Street], st AS [State] FROM OpenQuery(ADSI, ' SELECT UserPrincipalName, DisplayName, sAMAccountName, sn, givenName, department, title, Mail, l, postalCode, streetAddress, st FROM ''LDAP://mydomain.de/DC=mydomain,DC=de'' WHERE objectClass = ''User'' AND objectCategory = ''Person'' AND sn=''*'' AND givenName = ''*'' ') In most cases you’re done with that … except your organisation has more the 900 users! Then you have to split the fetch in several requests, because SQL Server quits with an error, when trying to read more than 900 records via ADSI. Best option is, to filter the ADSI statement by something like ‘get all user starting with a to j’, when you are sure, that in this case less than 900 records will be given back and repeat the statement several times and glue the data together via a UNION statement: SELECT UserPrincipalName, DisplayName, sAMAccountName AS [SamAccountName], sn AS [LastName], givenName AS [FirstName], title AS [Title], Mail as [MailAddress], department AS [Department], l AS [Location], postalCode AS [PostCode], streetAddress AS [Street], st AS [State] FROM ( SELECT * FROM OpenQuery(ADSI, ' SELECT UserPrincipalName, DisplayName, sAMAccountName, sn, givenName, department, title, Mail, l, postalCode, streetAddress, st FROM ''LDAP://mydomain.de/DC=mydomain,DC=de'' WHERE objectClass = ''User'' AND objectCategory = ''Person'' AND sn=''*'' AND givenName = ''*'' AND sAMAccountName <= ''j'' ') UNION ALL SELECT * FROM OpenQuery(ADSI, ' SELECT [...same as above] FROM ''LDAP://mydomain.de/DC=mydomain,DC=de'' WHERE objectClass = ''User'' AND objectCategory = ''Person'' AND sn=''*'' AND givenName = ''*'' AND sAMAccountName > ''j'' AND sAMAccountName < ''p'' ') UNION ALL SELECT * FROM OpenQuery(ADSI, ' SELECT [...same as above] FROM ''LDAP://mydomain.de/DC=mydomain,DC=de'' WHERE objectClass = ''User'' AND objectCategory = ''Person'' AND sn=''*'' AND givenName = ''*'' AND sAMAccountName >= ''p'' ') ) AD When you store this as a VIEW, you can join it wherever you want on SQL Server: CREATE VIEW [dbo].[vADUsers] AS [...SQL code from above] GO Step 3 - SQL Server Database ProjectIf you work with a SQL Server Database Project, to have the complete structure of your database available in a version control system, you will get some reference errors on compiling and publishing your newly added SQL View vADUsers and on some objects, which rely on this View, because of following problems: Project doesn’t know the Linked Server ADSI The structure (fields) of the data source is unknown Declare the Linked ServerTo show the Project that there is a Linked Server called ADSI, just add following lines at the start of your view: sp_addlinkedserver 'ADSI' GO CREATE VIEW [dbo].[vADUsers] AS [...SQL code from above] This mimics the adding of a Linked Server, but will be ignored by SQL Server on publish, because you already have a Linked Server with this name. The project is happy with it. Declare the data structureWhen you use the SQL-View vADUsers in a Stored Procedure for example, this object won’t compile, because the project knows nothing about the fields of the ADSI data source. The SELECT in the view is not enough. You have to add an empty SELECT to the View vADUsers, just for the declaration of the fields and without returning any records and join this via UNION with the other statements: sp_addlinkedserver 'ADSI' GO CREATE VIEW [dbo].[vtADAllUsers] AS SELECT UserPrincipalName, DisplayName, sAMAccountName AS [SamAccountName], sn AS [LastName], givenName AS [FirstName], title AS [Title], Mail as [MailAddress], department AS [Department], l AS [Location], postalCode AS [PostCode], streetAddress AS [Street], st AS [State] FROM ( -- Fake SELECT to declare the structure of the view SELECT TOP 0 '' UserPrincipalName, '' DisplayName, '' sAMAccountName, '' sn, '' givenName, '' department, '' title, '' Mail, '' l, '' postalCode, '' streetAddress, '' st UNION ALL SELECT * FROM OpenQuery(ADSI, ' SELECT UserPrincipalName, DisplayName, sAMAccountName, sn, givenName, department, title, Mail, l, postalCode, streetAddress, st FROM ''LDAP://mydomain.de/DC=mydomain,DC=de'' WHERE objectClass = ''User'' AND objectCategory = ''Person'' AND sn=''*'' AND givenName = ''*'' AND sAMAccountName >= ''j'' ') UNION ALL SELECT * FROM OpenQuery(ADSI, ' SELECT [...same as above] FROM ''LDAP://mydomain.de/DC=mydomain,DC=de'' WHERE objectClass = ''User'' AND objectCategory = ''Person'' AND sn=''*'' AND givenName = ''*'' AND sAMAccountName < ''j'' AND sAMAccountName > ''p'' ') UNION ALL SELECT * FROM OpenQuery(ADSI, ' SELECT [...same as above] FROM ''LDAP://mydomain.de/DC=mydomain,DC=de'' WHERE objectClass = ''User'' AND objectCategory = ''Person'' AND sn=''*'' AND givenName = ''*'' AND sAMAccountName <= ''p'' ') ) AD Now, you can fetch data from Active Directory and store the code in a Database Project properly. HAPPY CODING :)","categories":[{"name":"SQL","slug":"SQL","permalink":"https://kiko.io/categories/SQL/"}],"tags":[{"name":"ADSI","slug":"ADSI","permalink":"https://kiko.io/tags/ADSI/"},{"name":"Visual Studio","slug":"Visual-Studio","permalink":"https://kiko.io/tags/Visual-Studio/"},{"name":"Database Project","slug":"Database-Project","permalink":"https://kiko.io/tags/Database-Project/"}]}],"categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"},{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"},{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"},{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"},{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"},{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"},{"name":".NET","slug":"NET","permalink":"https://kiko.io/categories/NET/"},{"name":"SQL","slug":"SQL","permalink":"https://kiko.io/categories/SQL/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"},{"name":"2. Bundesliga","slug":"2-Bundesliga","permalink":"https://kiko.io/tags/2-Bundesliga/"},{"name":"Fediverse","slug":"Fediverse","permalink":"https://kiko.io/tags/Fediverse/"},{"name":"IndieWeb","slug":"IndieWeb","permalink":"https://kiko.io/tags/IndieWeb/"},{"name":"Identity","slug":"Identity","permalink":"https://kiko.io/tags/Identity/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"},{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"Trello","slug":"Trello","permalink":"https://kiko.io/tags/Trello/"},{"name":"Android","slug":"Android","permalink":"https://kiko.io/tags/Android/"},{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Theming","slug":"Theming","permalink":"https://kiko.io/tags/Theming/"},{"name":"Eintracht","slug":"Eintracht","permalink":"https://kiko.io/tags/Eintracht/"},{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Metadata","slug":"Metadata","permalink":"https://kiko.io/tags/Metadata/"},{"name":"Social Media","slug":"Social-Media","permalink":"https://kiko.io/tags/Social-Media/"},{"name":"Mastodon","slug":"Mastodon","permalink":"https://kiko.io/tags/Mastodon/"},{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/tags/JavaScript/"},{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"Git/GitHub","slug":"Git-GitHub","permalink":"https://kiko.io/tags/Git-GitHub/"},{"name":"Concert","slug":"Concert","permalink":"https://kiko.io/tags/Concert/"},{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"},{"name":"Node.js","slug":"Node-js","permalink":"https://kiko.io/tags/Node-js/"},{"name":"Meta","slug":"Meta","permalink":"https://kiko.io/tags/Meta/"},{"name":"UI","slug":"UI","permalink":"https://kiko.io/tags/UI/"},{"name":"Usability","slug":"Usability","permalink":"https://kiko.io/tags/Usability/"},{"name":"jQuery","slug":"jQuery","permalink":"https://kiko.io/tags/jQuery/"},{"name":"Contributing","slug":"Contributing","permalink":"https://kiko.io/tags/Contributing/"},{"name":"Templating","slug":"Templating","permalink":"https://kiko.io/tags/Templating/"},{"name":"JSON-LD","slug":"JSON-LD","permalink":"https://kiko.io/tags/JSON-LD/"},{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Presets","slug":"Presets","permalink":"https://kiko.io/tags/Presets/"},{"name":"Search","slug":"Search","permalink":"https://kiko.io/tags/Search/"},{"name":"Audio","slug":"Audio","permalink":"https://kiko.io/tags/Audio/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"},{"name":"Hosting","slug":"Hosting","permalink":"https://kiko.io/tags/Hosting/"},{"name":"SPA","slug":"SPA","permalink":"https://kiko.io/tags/SPA/"},{"name":"PWA","slug":"PWA","permalink":"https://kiko.io/tags/PWA/"},{"name":"Remote","slug":"Remote","permalink":"https://kiko.io/tags/Remote/"},{"name":"Workflow","slug":"Workflow","permalink":"https://kiko.io/tags/Workflow/"},{"name":"Bundling","slug":"Bundling","permalink":"https://kiko.io/tags/Bundling/"},{"name":"SVG","slug":"SVG","permalink":"https://kiko.io/tags/SVG/"},{"name":"Font","slug":"Font","permalink":"https://kiko.io/tags/Font/"},{"name":"Mail","slug":"Mail","permalink":"https://kiko.io/tags/Mail/"},{"name":"Office","slug":"Office","permalink":"https://kiko.io/tags/Office/"},{"name":"Visual Studio","slug":"Visual-Studio","permalink":"https://kiko.io/tags/Visual-Studio/"},{"name":"Logging","slug":"Logging","permalink":"https://kiko.io/tags/Logging/"},{"name":"C#","slug":"C","permalink":"https://kiko.io/tags/C/"},{"name":"Rant","slug":"Rant","permalink":"https://kiko.io/tags/Rant/"},{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"},{"name":"Tutorial","slug":"Tutorial","permalink":"https://kiko.io/tags/Tutorial/"},{"name":"DOM","slug":"DOM","permalink":"https://kiko.io/tags/DOM/"},{"name":"ES6","slug":"ES6","permalink":"https://kiko.io/tags/ES6/"},{"name":"Share","slug":"Share","permalink":"https://kiko.io/tags/Share/"},{"name":"WebAPI","slug":"WebAPI","permalink":"https://kiko.io/tags/WebAPI/"},{"name":"Authentication","slug":"Authentication","permalink":"https://kiko.io/tags/Authentication/"},{"name":"Localization","slug":"Localization","permalink":"https://kiko.io/tags/Localization/"},{"name":"Debugging","slug":"Debugging","permalink":"https://kiko.io/tags/Debugging/"},{"name":"Events","slug":"Events","permalink":"https://kiko.io/tags/Events/"},{"name":"PowerShell","slug":"PowerShell","permalink":"https://kiko.io/tags/PowerShell/"},{"name":"MediaQuery","slug":"MediaQuery","permalink":"https://kiko.io/tags/MediaQuery/"},{"name":"Error","slug":"Error","permalink":"https://kiko.io/tags/Error/"},{"name":"Stylus","slug":"Stylus","permalink":"https://kiko.io/tags/Stylus/"},{"name":"Versioning","slug":"Versioning","permalink":"https://kiko.io/tags/Versioning/"},{"name":"T4","slug":"T4","permalink":"https://kiko.io/tags/T4/"},{"name":"Resource","slug":"Resource","permalink":"https://kiko.io/tags/Resource/"},{"name":"TFS/DevOps","slug":"TFS-DevOps","permalink":"https://kiko.io/tags/TFS-DevOps/"},{"name":"Dark Mode","slug":"Dark-Mode","permalink":"https://kiko.io/tags/Dark-Mode/"},{"name":"ADSI","slug":"ADSI","permalink":"https://kiko.io/tags/ADSI/"},{"name":"Database Project","slug":"Database-Project","permalink":"https://kiko.io/tags/Database-Project/"}]}
\ No newline at end of file
+{"meta":{"title":"kiko.io","subtitle":"Memorable (Tech) Stuff","description":"Blog about memorable (tech) stuff by Kristof Zerbe","author":"Kristof Zerbe","url":"https://kiko.io","root":"/"},"pages":[{"title":"","date":"2022-12-30","updated":"2022-12-30","path":"index.html","permalink":"https://kiko.io/index.html","excerpt":"","text":"TODO: Extract text from index.ejs"},{"title":"404","date":"2020-09-23","updated":"2020-09-23","path":"404.html","permalink":"https://kiko.io/404.html","excerpt":"","text":"I don’t know how you ended up here, but you have jumped over the edge of this blog. Maybe it’s the end of the internet and you can power off your machine now… /@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@ @@@@@@@@@ ,@@@@@ @@@@@@@@@@ @@@@@@@@@ @@@@@@@@@@ ,@@@@@@@@@@@@ @@@@@@@@@ @@@@@@@@@@@@ @@@@@@@@@@@@ @@@@@@@@@ (@@@@@@@@@@@@ #@@@@@@@@@@@ @@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@ @@@@@@@@@@, @@@@@@@@@. @@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@ @@@@@@@@@ @@@@@@@@@@ @@@@@@@@@ @@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@@@ @@@@@@@@@@@@@ /@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@ &@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@. *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@ … or you climb back and read one of my recent posts :)"},{"title":"Impressum","date":"2023-01-09","updated":"2023-01-09","path":"impressum/index.html","permalink":"https://kiko.io/impressum/index.html","excerpt":"","text":"The following information is required under German law. Verantwortlich für den Inhalt gemäß § 55 Abs.2 RStVKristof ZerbeKaiser-Friedrich-Str. 865193 Wiesbaden Mobil: +49 171 6998867 kristof.zerbe@gmail.com Rechtliche HinweiseHiermit wird darauf hingewiesen, dass Informationen auf dieser Site technische Ungenauigkeiten oder typografische Fehler enthalten können. Es gilt der Vorbehalt, dass die Informationen dieser Site jederzeit und ohne vorherige Ankündigung geändert oder aktualisiert werden können. Alle auf diesem Blog veröffentlichten Werke bzw. Werkteile wie z.B. Texte, Dateien, Kompositionen und Bilder sind urheberrechtlich geschützt. Jede weitere Veröffentlichung, Vervielfältigung, Verbreitung oder sonstige Nutzung – auch auszugsweise – bedarf der Zustimmung des Seitenbetreibers. Die Inhalte der Seiten wurden sorgfältig geprüft und nach bestem Wissen erstellt. Dennoch wird für die hier bereitgestellten Informationen kein Anspruch auf Vollständigkeit, Aktualität, Qualität und Richtigkeit erhoben. Für Schäden, die durch das Vertrauen auf die Inhalte dieser Website oder deren Gebrauch entstehen, wird keine Haftung übernommen. Dies gilt insbesondere für Inhalte verlinkter Webseiten, für deren Inhalte ausschließlich deren Betreiber verantwortlich sind. DatenschutzDie anwendbaren datenschutzrechtlichen Bestimmungen werden bei der Erhebung, bei der Nutzung und bei der Verarbeitung personenbezogener Daten beachtet. Sofern innerhalb des Internet-Angebotes die Möglichkeit der Eingabe von persönlichen Daten (E-Mail-Adresse oder Namen) besteht, erfolgt diese freiwillig. Die von Ihnen zur Verfügung gestellten personenbezogenen Daten werden nur intern zur Kommunikation mit Ihnen verwendet. HostingDiese Website wird auf GitHub Pages gehostet. Wenn eine GitHub Pages-Website besucht wird, wird die IP-Adresse des Besuchers zu Sicherheitszwecken protokolliert und gespeichert, unabhängig davon, ob sich der Besucher bei GitHub angemeldet hat oder nicht. Weitere Informationen zu den Sicherheitspraktiken von GitHub sind in den GitHub-Datenschutzbestimmungen zu finden. KommentareDie Kommentare auf dieser Website werden von der Open-Source-Bibliothek Utterances ermöglicht, das ebenfalls auf GitHub gehostet wird. Von dort wird der Kommentar-Client nachgeladen und auf der entsprechenden Artikelseite angezeigt. Um zu kommentieren ist eine Anmeldung mittels Ihres eigenen GitHub-Kontos notwendig. Die Kommentare werden im GitHub-Repository als sogenanntes ‘Issue’ an der Artikeldatei dieser Webseite mit einer Verknüpfung zu Ihren GitHub-Profilinformationen gespeichert. Weitere Informationen ergeben sich aus dem obigen Abschnitt ‘Hosting’. WebmentionsWebmentions sind ein Mechanismus, mit dem andere Websites benachrichtigt werden, wenn Sie auf Ihrer eigenen Website auf sie verweisen. Indem Sie Webmentions auf Ihrer Website unterstützen, signalisieren Sie ausdrücklich, dass Sie möchten, dass die verlinkten Websites Ihre öffentlichen Antworten auf deren Inhalte verarbeiten und veröffentlichen. Sie können jederzeit die Entfernung einer oder aller Webmentions, die von Ihrer Website stammen, beantragen. In der Regel werden empfangene Webmentions als Kommentare auf einer Webseite angezeigt. Dies bedeutet, dass eine Kopie Ihres Inhalts auf der Website angezeigt wird, auf die Sie in Ihrem eigenen Beitrag verlinken. Eine eingehende Webmention-Anfrage wird als Aufforderung zur Verarbeitung personenbezogener Daten behandelt und ist somit von vornherein eine Anfrage zur Veröffentlichung eines Kommentars von einer anderen Stelle im Web. Dafür wurde das Protokoll entwickelt und deshalb ist es auf Ihrer Website aktiv. Zu den veröffentlichten persönlichen Daten gehören Ihr Name, Ihr Profilbild von Ihrer Website oder einem gleichartigen Dienst, die URL Ihrer Website und persönliche Informationen, die Sie möglicherweise in Ihrem Beitrag angegeben haben. Die Veröffentlichung eingehender Webmentions basiert auf dem berechtigten Interesse, die Interaktion mit den Lesern dieser Website zu ermöglichen (Art. 6 (1) DSGVO), und folgt der Designabsicht des Webmention-Protokolls. Analyse-Tool PirschAuf dieser Website kommt das Open-Source-Webanalysedienst Pirsch.io der Emvi Software GmbH, Nickelstraße 1b, 33378 Rheda-Wiedenbrück mit Sitz in Deutschland zum Einsatz, um die Nutzung der Website analysieren und regelmäßig verbessern zu können. Die Technologie von Pirsch ist eine Cookie-freie und datenschutzfreundliche Web-Analytik, um unsere Traffic genau messen zu können. Pirsch wird in Deutschland entwickelt und gehostet und arbeitet somit nach den strengen europäischen Datenschutzgesetzen der DSGVO. Die Analysedaten, welche erhoben werden, werden zu keiner Zeit an Dritte weitergegeben. Es werden mit der Technologie von Pirsch ausschließlich anonymisierte Daten (z.B. Datum und Uhrzeit des Seitenaufrufs, Verweildauer oder die Seite, von der sie unsere Website angesteuert haben) gespeichert und verwendet. Sie erlauben keine Identifikation der Besucher dieser Website. Rechtsgrundlage für die Nutzung von Pirsch ist Art. 6 Abs. 1 f) DSGVO. Sie haben die Möglichkeit, der Analyse zu widersprechen. Informationen zum Datenschutz von Pirsch erhalten sie unter: https://pirsch.io/privacy."},{"title":"","date":"2023-12-12","updated":"2023-12-12","path":"downloads/code/test.js","permalink":"https://kiko.io/downloads/code/test.js","excerpt":"","text":"alert('This is a test...'); console.log('This is a test...');"}],"posts":[{"title":"SVWW vs. Braunschweig @ 2023-12-08","subtitle":"Third defeat in a row, this time at home","series":"SV Wehen Wiesbaden","date":"2023-12-10","updated":"2023-12-10","path":"post/SVWW-vs-Braunschweig-2023-12-08/","permalink":"https://kiko.io/post/SVWW-vs-Braunschweig-2023-12-08/","excerpt":"1:3 Our last home game of the year. The last two away games against Greuther Fürth and Holstein Kiel were unfortunately lost. One 2:0 and the other 3:2, so you could say that our good run had come to an end. But well … anyone who thought it would go on like this should have seen a doctor. After all, we’re the promoted team and we have to win against far stronger teams with far more Bundesliga experience. We can only ever catch them at a weak moment and hope that in the end it will be enough to stay in the 2. Bundesliga. Eintracht Braunschweig was one of the teams we thought had a chance, as they were in a relegation spot with just 8 points before the matchday (we have 21) and hadn’t won an away game for months.","keywords":"home game year games greuther fürth holstein kiel lost good run end … thought doctor promoted team win stronger teams bundesliga experience catch weak moment hope stay eintracht braunschweig chance relegation spot points matchday hadnt won months","text":"1:3 Our last home game of the year. The last two away games against Greuther Fürth and Holstein Kiel were unfortunately lost. One 2:0 and the other 3:2, so you could say that our good run had come to an end. But well … anyone who thought it would go on like this should have seen a doctor. After all, we’re the promoted team and we have to win against far stronger teams with far more Bundesliga experience. We can only ever catch them at a weak moment and hope that in the end it will be enough to stay in the 2. Bundesliga. Eintracht Braunschweig was one of the teams we thought had a chance, as they were in a relegation spot with just 8 points before the matchday (we have 21) and hadn’t won an away game for months. It’s already very cold in Wiesbaden at the beginning of December, especially as the weather was bringing in a lot of cold air from the north, so I left the scooter and took a cab to the Brita Arena. Freezing sucks and as I no longer have the heat of the youth, I bought long underwear for winter stadium visits. (Uhh, the old man wears long underpants). I was glad to have them though, because on the one hand I was there way too early and on the other hand you end up sitting for most of the 120 minutes that a game like this lasts, including waiting for kick-off and the interval. The hot cider and warm beef sausages only helped to combat the cold to a limited extent. Bärbel did better, as she arrived shortly before kick-off, like most of the 7,200 spectators. We chatted a bit about trivialities and hoped that our boys wouldn’t let the opportunity pass them by today. The GameThe coach seemed to have had something similar in mind, because as soon as the ball started rolling, our boys attacked their opponents and had their first 100% chance after just 20 seconds (!). A few minutes later, the ball was actually in the goal, but it was probably disallowed by the referee for offside or something similar. I don’t know but it didn’t matter as long as it continued at this pace. Braunschweig were hopelessly out of their depth and simply tried to prevent the inevitable … until the 18th minute … 1:0 - A fine header by our defender Aleksandar Vukotic :) However, the problem was that our team then let themselves go a little and only benefited from the opponent’s harmlessness until the break. As if the job was already done… let macy_k5yygd = new Macy({ container: '#image-masonry-k5yygd', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); Bärbel and I hoped that the coach in the dressing room would be able to shake the boys up again and they would play the second half with the same energy of the first 20 minutes. But what came was a bitter disappointment. Braunschweig’s coach seemed to have achieved exactly what we had hoped for. Our opponents came onto the pitch and started playing aggressive football. In the 48th minute, the score was 1:1 after a counterattack and only 8 minutes later, Braunschweig had turned the game around: 1:2. The lack of resistance and insecurity on our side was problematic. Hardly a ball got to where it needed to go, hardly a duel was won and even 5 substitutions unfortunately did nothing to change this. The boys were somehow beside themselves and so Braunschweig put the game to bed in the 76th minute: 1:3 :| ⇾ Match report on kicker.de ConclusionThe stadium has rarely emptied as quickly as it did this time, but that wasn’t just due to the cold, but also the disappointment. Unfortunately, it wasn’t a Christmas present from the players to their fans in the last home game of 2023. The team have to play one last away game next week at St. Pauli before the break, but nobody expects a win against the strongest team in the 2nd division at the moment. No matter … we are in mid-table, which is more than we had hoped for at the start of the season. In 7 weeks time, we’ll be back in the Brita-Arena against Herta BSC and we’ll see better games again and stay in the league!","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"},{"name":"2. Bundesliga","slug":"2-Bundesliga","permalink":"https://kiko.io/tags/2-Bundesliga/"}]},{"title":"Hexo, WebFinger and better discoverability","subtitle":"Providing a static Webfinger file via your own domain","series":"A New Blog","date":"2023-12-02","updated":"2023-12-02","path":"post/Hexo-Webfinger-and-better-discoverability/","permalink":"https://kiko.io/post/Hexo-Webfinger-and-better-discoverability/","excerpt":"Recently I read the blog post “Mastodon on your own domain without hosting a server” by Maarten Balliauw, which dealt with how to become more visible in the Fediverse, more precisely in Mastodon, with your own domain, because in contrast to the Indieweb approach, the Fediverse relies on Actors (@USER@INSTANCE) of the respective instance/platform and can only include your own domain, if it becomes a Fediverse endpoint itself. In my case, the latter is not possible because this blog is a static site, generated via Hexo and hosted on GitHub. It simply lacks a modifiable active server component. However, Maarten has found a trick to at least make it findable in Mastadon via his own domain. First, he explains how Fediverse platforms work in general: – Mastodon (and others) use ActivityPub as their protocol to communicate between “actors”.– Actors are discovered using WebFinger, a way to attach information to an email address, or other online resource.– WebFinger lives on /.well-known/webfinger on a server. His idea was to simply copy the WebFinger file to his server and make it available in the same way, to allow the Fediverse server to find the correct actor, so search for @me@mydomain.xxx and find @me@my-fediverse-instance.xxx. Copy a file and deliver it via Hexo over .wellknown/webfinger? What can be so difficult about that…","keywords":"recently read blog post mastodon domain hosting server maarten balliauw dealt visible fediverse precisely contrast indieweb approach relies actors @user@instance respective instance/platform include endpoint case static site generated hexo hosted github simply lacks modifiable active component found trick make findable mastadon explains platforms work general – activitypub protocol communicate actors– discovered webfinger attach information email address online resource– lives /well-known/webfinger idea copy file find correct actor search @me@mydomainxxx @me@my-fediverse-instancexxx deliver wellknown/webfinger difficult that…","text":"Recently I read the blog post “Mastodon on your own domain without hosting a server” by Maarten Balliauw, which dealt with how to become more visible in the Fediverse, more precisely in Mastodon, with your own domain, because in contrast to the Indieweb approach, the Fediverse relies on Actors (@USER@INSTANCE) of the respective instance/platform and can only include your own domain, if it becomes a Fediverse endpoint itself. In my case, the latter is not possible because this blog is a static site, generated via Hexo and hosted on GitHub. It simply lacks a modifiable active server component. However, Maarten has found a trick to at least make it findable in Mastadon via his own domain. First, he explains how Fediverse platforms work in general: – Mastodon (and others) use ActivityPub as their protocol to communicate between “actors”.– Actors are discovered using WebFinger, a way to attach information to an email address, or other online resource.– WebFinger lives on /.well-known/webfinger on a server. His idea was to simply copy the WebFinger file to his server and make it available in the same way, to allow the Fediverse server to find the correct actor, so search for @me@mydomain.xxx and find @me@my-fediverse-instance.xxx. Copy a file and deliver it via Hexo over .wellknown/webfinger? What can be so difficult about that… Download the WebFinger fileAs Maarten describes, the WebFinger file (JSON) can always be found on a URL according to the following pattern:https://<your mastodon server>/.well-known/webfinger?resource=acct:<your account>@<your mastodon server>. I have my Mastodon account ‘kiko’ at indieweb.social and the path to the download was accordingly: https://indieweb.social/.well-known/webfinger?resource=acct:kiko@indieweb.social. Solution 1 - Copy and provide as static fileFirst of all, Hexo knows nothing about static files. You have to teach it, for example, using the hexo-generator-copy plugin, which I did a long time ago because I deliver a lot of such files, like images, photos, manifests and so on. Including the WebFinger file in the .wellknown folder in the generation was therefore an obvious choice … but did not work, as the plugin ignores all files and folders that begin with a dot or underscore. This doesn’t matter on Windows, but it probably does on other platforms, so I wanted to leave the plugin’s code untouched and wrote another generator that then generates the file separately into the output: generator-wellknown-webfinger.js1234567891011121314151617181920212223242526const log = require('hexo-log')({ debug: false, silent: false });const path = require('path');const fs = require('hexo-fs');hexo.extend.generator.register("wellknown-webfinger", async function() { log.info("Processing .well-known/webfinger ..."); const _rootPath = hexo.base_dir; const _path = ".well-known/webfinger"; let content = ""; let filePath = path.join(_rootPath, this.config.static_dir, _path); if (fs.existsSync(filePath)) { json = JSON.parse(fs.readFileSync(filePath)); content = JSON.stringify(json); // flatten JSON } let result = { data: content, path: _path }; return result;}); The whole thing is not magic, because it simply reads the local file from /static/.wellknown/webfinger and returns the output path and the flattened content to be rendered as the result. But somehow it didn’t feel right to first download a file manually and then bypass a generator with another generator in order to deliver the file. Solution 2 - Download and deliver during generationA short time later, I deleted the local file again and rewrote the generator, because the first solution didn’t make sense to me. Generating my blog now takes less than a minute anyway, so it doesn’t matter whether I fetch the WebFinger file directly from the server and write it straight to the output via hexo.route.set: generator-wellknown-webfinger.js123456789101112131415161718const log = require('hexo-log')({ debug: false, silent: false });const axios = require("axios");hexo.extend.generator.register("wellknown-webfinger", async function() { log.info("Processing .well-known/webfinger ..."); const url = `https://${this.config.mastodon.server}/.well-known/webfinger?resource=acct:${this.config.mastodon.user}@${this.config.mastodon.server}`; const _path = ".well-known/webfinger"; axios.get(url).then(response => { let json = response.data; hexo.route.set(_path, json); });}); The ResultAfter deploying to GitHub I was able to search on a Fediverse platform for @kristof@kiko.io and the result is the corresponding account of my Mastodon instance. :) However, this currently only works with one instance and the user specification is arbitrary (as Maarten also notes). For example, if I search on Pixelfed, my Mastodon account is also displayed, but not my Pixelfed account, which is also available there, because I have not integrated Pixelfeds WebFinger file. As mentioned above, the Fediverse works a little differently than the Indieweb. My wish would be to find something that identifies me with my domain everywhere. More Info IndieWebCamp: WebFingerBrandon Rozek: Mastodon/Webfinger Alias using HTTP RedirectsGitHub Issue from r3pek at webfinger.net: Is webfinger able to distinguish between 2 services on the same URI?","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Fediverse","slug":"Fediverse","permalink":"https://kiko.io/tags/Fediverse/"},{"name":"IndieWeb","slug":"IndieWeb","permalink":"https://kiko.io/tags/IndieWeb/"},{"name":"Identity","slug":"Identity","permalink":"https://kiko.io/tags/Identity/"}]},{"title":"Mecklenburg Lakes","subtitle":"Impressions of a Houseboat Trip","series":"New Photos","date":"2023-11-26","updated":"2023-11-26","path":"post/Mecklenburg-Lakes/","permalink":"https://kiko.io/post/Mecklenburg-Lakes/","excerpt":"August in Germany is usually a safe bet when it comes to the weather, so my wife and I decided to relax for a few days on the Mecklenburg Lake Plateau … on a chartered boat. In general, you need a driving license for everything that moves in this country, but there are a few exceptions, such as in Brandenburg/Mecklenburg where even inexperienced tourists are allowed to steer a 12-metre houseboat/yacht across the many lakes after a short briefing. The weather was, let’s say, suboptimal this August, because a storm passed over us in the first three days and there was no chance of happy sailing around on the water. On the one hand, the swell and wind forces were extremely worrying for newbies and on the other, it’s simply no fun being on a boat in cold, heavy rain. We were stuck in Rheinsberg. On the one hand, the swell and wind forces were extremely worrying for newbies and on the other, it’s simply no fun being on a boat in cold, heavy rain.","keywords":"august germany safe bet weather wife decided relax days mecklenburg lake plateau … chartered boat general driving license moves country exceptions brandenburg/mecklenburg inexperienced tourists allowed steer 12-metre houseboat/yacht lakes short briefing lets suboptimal storm passed chance happy sailing water hand swell wind forces extremely worrying newbies simply fun cold heavy rain stuck rheinsberg","text":"August in Germany is usually a safe bet when it comes to the weather, so my wife and I decided to relax for a few days on the Mecklenburg Lake Plateau … on a chartered boat. In general, you need a driving license for everything that moves in this country, but there are a few exceptions, such as in Brandenburg/Mecklenburg where even inexperienced tourists are allowed to steer a 12-metre houseboat/yacht across the many lakes after a short briefing. The weather was, let’s say, suboptimal this August, because a storm passed over us in the first three days and there was no chance of happy sailing around on the water. On the one hand, the swell and wind forces were extremely worrying for newbies and on the other, it’s simply no fun being on a boat in cold, heavy rain. We were stuck in Rheinsberg. On the one hand, the swell and wind forces were extremely worrying for newbies and on the other, it’s simply no fun being on a boat in cold, heavy rain. Not only because it felt like luxury camping without people but only water around, but also because it gave a pleasant feeling of freedom. Yes, we weren’t nearly alone on the lakes and getting a good spot in a marina for electricity and fresh water wasn’t always easy, but then just anchoring in any lake for the night and being woken up by birdsong and a gentle swell was magical. Below are a few pictures from this trip, some of which will be used again as hero images in this blog: Grinning Horse Sea Magic Rainy Tourism Rainy Planks Chrome Bollard Rheinsberg Palace Pink Lotus Stony Vegetation Golden Drama Buddy Support Stormy Lake Green Canal Boiler Mirroring Into the Storm Sunset Jetty Golden Surfing Blue Lake Green Ducks Sparkling Duck Mirrored Boathouses","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"Discoveries #27 - JavaScript Tools","subtitle":null,"series":"Discoveries","date":"2023-11-20","updated":"2023-11-20","path":"post/Discoveries-27-JavaScript-Tools/","permalink":"https://kiko.io/post/Discoveries-27-JavaScript-Tools/","excerpt":"This month's Discoveries are about small, large, new and somewhat older JavaScript tools that can make life and coding easier for developers. Why reinvent the wheel when someone has already done it. Happy Coding :) Spacing.js - MeasuringUkiyo.js - Parallax EffectIntersectionObserver Debuggermande - Fetch WrapperVest - Declarative Validations FrameworkGranim.js - Gradient AnimationRoughNotationbarba.js - Page Transitionsdead-or-alive - URL Checkertimeago - Format Date","keywords":"month's discoveries small large older javascript tools make life coding easier developers reinvent wheel happy spacingjs - measuringukiyojs parallax effectintersectionobserver debuggermande fetch wrappervest declarative validations frameworkgranimjs gradient animationroughnotationbarbajs page transitionsdead-or-alive url checkertimeago format date","text":"This month's Discoveries are about small, large, new and somewhat older JavaScript tools that can make life and coding easier for developers. Why reinvent the wheel when someone has already done it. Happy Coding :) Spacing.js - MeasuringUkiyo.js - Parallax EffectIntersectionObserver Debuggermande - Fetch WrapperVest - Declarative Validations FrameworkGranim.js - Gradient AnimationRoughNotationbarba.js - Page Transitionsdead-or-alive - URL Checkertimeago - Format Date Spacing.js - Measuring by Steven Lei https://spacingjs.com/ Have you ever wanted to measure your web layout during development? Steven has a solution for you. Either as JavaScript integrated in HTML or as a Chrome plugin. Ukiyo.js - Parallax Effect by Yiteng Jun https://github.com/yitengjun/ukiyo-js This ES6 library is an easy to use tool for creating efficient background parallax effects for <img>, <picture>, <video> elements and background images. It can be called manually via JavaScript or automatically via a special class or data attribute. IntersectionObserver Debugger by Rodrigo Pombo https://github.com/pomber/intersection-observer-debugger This tiny script is a debugging tool, included during development, which shows you the root, target, and intersection every time an IntersectionObserver is triggered. mande - Fetch Wrapper by Eduardo San Martin Morote https://github.com/posva/mande With mande Eduardo has written a great wrapper for JavaScripts fetch(), which not only relieves you of a lot of typing work, but also comes with a few useful extensions, e.g. for nuxt. This turns an API call into a one-liner. Vest - Declarative Validations Framework by Evyatar Alush https://github.com/ealush/vest Vest is a declarative validation framework for validating form input that is as easy to use as the Mocha or Jest unit test libraries. Granim.js - Gradient Animation by Benjamin Blonde https://sarcadass.github.io/granim.js/examples.html Benjamin has created a library to animate gradients as complex as you need them. It supports pausing when it's not in view and different blending modes for images. RoughNotation by Preet Shihn https://roughnotation.com/ To emphasize something important on a sheet of paper, you sometimes use a highlighter. This library brings that to the web in an animated way. With a circle around it, underlined … or both, you're sure to attract attention. barba.js - Page Transitions by Multiple Contributors https://barba.js.org/features/user-friendly/ Barba is a library that allows you to create fluid and smooth transitions between pages on your website, while we wait for the View Transitions API. dead-or-alive - URL Checker by Titus Wormer https://github.com/wooorm/dead-or-alive This URL checker ensures that links do not lead to nothing, whether on websites, in node projects or in service workers. It even checks anchor links for the presence of the element. timeago - Format Date by Whoever Titanium is... https://github.com/Titanium2099/timeago 'Your message was sent 5 minutes ago'. Notes like these are easy to bring into a web application with this script. It currently only supports English, but is easily customizable.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Speyer Automotive","subtitle":null,"series":"New Photos","date":"2023-11-15","updated":"2023-11-15","path":"post/Speyer-Automotive/","permalink":"https://kiko.io/post/Speyer-Automotive/","excerpt":"In September, my wife and I took a trip to the Museum of Technology in nearby Speyer. I don’t really know why I’ve never been there before, not even as a child, because the collection of cars and aeroplanes is quite unique in Germany. A dream for every boy, even if he’s not as young as he might think :) Built on the old site of an aircraft manufacturer, the 100-year-old hangars provide the perfect space for exhibiting large vehicles with wheels and wings. And exactly this can be found in Speyer: A large part for cars of all kinds and the outside area for aeroplanes and even ships. Even a spaceship, the Russian Space Shuttle alternative Buran can be seen there. Here are a few pictures of the car exhibition that I brought back from there and which will be used as post header images hero on kiko.io in the future …","keywords":"september wife trip museum technology nearby speyer dont ive child collection cars aeroplanes unique germany dream boy hes young built site aircraft manufacturer 100-year-old hangars provide perfect space exhibiting large vehicles wheels wings found part kinds area ships spaceship russian shuttle alternative buran pictures car exhibition brought back post header images hero kikoio future …","text":"In September, my wife and I took a trip to the Museum of Technology in nearby Speyer. I don’t really know why I’ve never been there before, not even as a child, because the collection of cars and aeroplanes is quite unique in Germany. A dream for every boy, even if he’s not as young as he might think :) Built on the old site of an aircraft manufacturer, the 100-year-old hangars provide the perfect space for exhibiting large vehicles with wheels and wings. And exactly this can be found in Speyer: A large part for cars of all kinds and the outside area for aeroplanes and even ships. Even a spaceship, the Russian Space Shuttle alternative Buran can be seen there. Here are a few pictures of the car exhibition that I brought back from there and which will be used as post header images hero on kiko.io in the future … Red Bull 12 Packard Monster Golden Cooler Vespa Treasure Yellow Curves Red Motor Cover Maybach Zeppelin Mercedes Gold Magirus Red","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"SVWW vs. Kaiserslautern @ 2023-11-12","subtitle":"The winning streak continues in curious fashion","series":"SV Wehen Wiesbaden","date":"2023-11-15","updated":"2023-11-15","path":"post/SVWW-vs-Kaiserslautern-2023-11-12/","permalink":"https://kiko.io/post/SVWW-vs-Kaiserslautern-2023-11-12/","excerpt":"2:1 Travelling by scooter in Germany in the middle of November is no fun. Just like on Sunday. Light rain at just under 9 degrees Celsius and quite a bit of wind. Brrr… but what can I do, there are no car parks at the arena. Not at all today, because it’s against Kaiserslautern, which is only about 100 kilometres away from Wiesbaden and therefore a lot of visitors are expected. It’s a kind of derby. 1. FC Kaiserslautern is one of the great traditional clubs in Germany, which has celebrated a total of 4 German Championships in its history, but has really lost momentum since its relegation to the 2. Bundesliga in 2006. Between 2018 and 2022, the team even only played in the 3rd division pf German football, but after their last year’s promotion, they are hoping to be promoted to the top division again soon. Recent results in the cup have shown that they have a strong team and it was to be feared that they would overrun Wehen Wiesbaden in their run, although their last league game was lost against Fürth. It was also rather uncomfortably cold in the stadium and I wondered once again how the young lads do it on the pitch in their shorts, especially the goalkeeper. But the fans created the right atmosphere. As expected, the entire guest stand was full. 12,100 spectators in total; a new season record. The SVWW fan block was also packed this time and the ultras had come up with a nice choreo with complete stand banners and red smoke (see photos).","keywords":"travelling scooter germany middle november fun sunday light rain degrees celsius bit wind brrr… car parks arena today kaiserslautern kilometres wiesbaden lot visitors expected kind derby fc great traditional clubs celebrated total german championships history lost momentum relegation bundesliga team played 3rd division pf football years promotion hoping promoted top recent results cup shown strong feared overrun wehen run league game fürth uncomfortably cold stadium wondered young lads pitch shorts goalkeeper fans created atmosphere entire guest stand full spectators season record svww fan block packed time ultras nice choreo complete banners red smoke photos","text":"2:1 Travelling by scooter in Germany in the middle of November is no fun. Just like on Sunday. Light rain at just under 9 degrees Celsius and quite a bit of wind. Brrr… but what can I do, there are no car parks at the arena. Not at all today, because it’s against Kaiserslautern, which is only about 100 kilometres away from Wiesbaden and therefore a lot of visitors are expected. It’s a kind of derby. 1. FC Kaiserslautern is one of the great traditional clubs in Germany, which has celebrated a total of 4 German Championships in its history, but has really lost momentum since its relegation to the 2. Bundesliga in 2006. Between 2018 and 2022, the team even only played in the 3rd division pf German football, but after their last year’s promotion, they are hoping to be promoted to the top division again soon. Recent results in the cup have shown that they have a strong team and it was to be feared that they would overrun Wehen Wiesbaden in their run, although their last league game was lost against Fürth. It was also rather uncomfortably cold in the stadium and I wondered once again how the young lads do it on the pitch in their shorts, especially the goalkeeper. But the fans created the right atmosphere. As expected, the entire guest stand was full. 12,100 spectators in total; a new season record. The SVWW fan block was also packed this time and the ultras had come up with a nice choreo with complete stand banners and red smoke (see photos). My nice seat neighbour, the older lady, was there again and we made friends this time. Her name is Bärbel and this time she was a bit more excited than normally. Well, the aim was to beat Kaiserslautern. Kaiserslautern! Every now and then she also started to grumble: about an opposing player or when things didn’t go fast enough forwards or when the ball was lost unnecessarily. Not nearly as blatant as the “gentlemen” behind us, who were swearing in one piece and indulging in football wisdom, but you could tell that the game was more important to her than others. The GameIn the first half, the game was rather well balanced, even if you had the feeling that our team was the only ones playing. There was no sign of Kaiserlautern’s attacking drive. Up until the 39th minute, we had one chance to score, but the visitors hadn’t even had a shot on our goal. Nothing, nada, niente. But then the ball was in our net: 0:1! A strange goal, because it didn’t look intentional. The ball just bounced at Ritter’s feet by chance and he simply took a shot. Goal. Damn… let macy_odjtmi = new Macy({ container: '#image-masonry-odjtmi', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); After the break, Kaiserslautern seemed to want to build on the goal. They were now playing football. And ours were probably angry at not having been rewarded for their efforts in the first half. The game became more lively, but we still had the better scenes … and after just 4 minutes, the visitors gave our Tijmen Goppel far too much time to aim and shoot just outside the penalty area and the ball was deflected into the goal by an opponent’s leg. 1:1! In the 65th minute, things got curious again: Goppel was fouled and awarded a free kick about 20 metres from goal. Robin Heußer took it and his shot somehow hit the chest of Ivan Prtajin in the penalty area, from where the ball went into the goal. 2:1 … Game turned round! The last half hour was really nerve-wracking. We continued to play better, but the pressure increased and the game became noticeably faster. Shortly before the end, substitute John Iredale ran alone with the ball across half the pitch and everyone held their breath, but just before the goal he tried to round the last defender and he was just able to scoop the ball away. G… damn it! ⇾ Match report on kicker.de ConclusionThis was the fourth (!) game in a row that we have won and our position in the table looks correspondingly favourable again. Only three points behind the promotion places. Woohoo … but let’s not fool ourselves: There will be another losing streak in the future (as always in football) and 21 points are still not enough to keep us in the league, and that’s all that matters. Two away games are scheduled in the next three weeks, against Greuther Fürth and Holstein Kiel. On 8 December, we return to the Brita Arena against Eintracht Braunschweig, currently second last in the table. They should be beatable…","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"},{"name":"2. Bundesliga","slug":"2-Bundesliga","permalink":"https://kiko.io/tags/2-Bundesliga/"}]},{"title":"Add Link to Trello on Android via Share Menu","subtitle":null,"date":"2023-11-13","updated":"2023-11-13","path":"post/Add-Link-to-Trello-on-Android-via-Share-Menu/","permalink":"https://kiko.io/post/Add-Link-to-Trello-on-Android-via-Share-Menu/","excerpt":"I have been collecting interesting links in various Trello boards for many years and also process some of them automatically, such as my Tiny Tools. However, the official Trello Android App has the problem eversince that, when you want to create a URL as a Trello card in the browser, the URL is entered in the Description and not as an Attachment, where it actually belongs. I have solved this for myself for years using a bookmarklet in the Chrome browser (see Add website to Trello card the better way) and get along quite well with it. However, there is one catch, that has annoyed me ever since:I find an interesting link in the Mastodon WebApp, for example, and tap on it. What opens, however, is the WebView integrated in Android and not my standard Chrome browser, in which the bookmarklet would be available. So, for links that I want to store, I always have to open the WebView menu and select “Open in Chrome Browser”. I cannot use the general SHARE menu. At least not so far … :)","keywords":"collecting interesting links trello boards years process automatically tiny tools official android app problem eversince create url card browser entered description attachment belongs solved bookmarklet chrome add website catch annoyed sincei find link mastodon webapp tap opens webview integrated standard store open menu select general share …","text":"I have been collecting interesting links in various Trello boards for many years and also process some of them automatically, such as my Tiny Tools. However, the official Trello Android App has the problem eversince that, when you want to create a URL as a Trello card in the browser, the URL is entered in the Description and not as an Attachment, where it actually belongs. I have solved this for myself for years using a bookmarklet in the Chrome browser (see Add website to Trello card the better way) and get along quite well with it. However, there is one catch, that has annoyed me ever since:I find an interesting link in the Mastodon WebApp, for example, and tap on it. What opens, however, is the WebView integrated in Android and not my standard Chrome browser, in which the bookmarklet would be available. So, for links that I want to store, I always have to open the WebView menu and select “Open in Chrome Browser”. I cannot use the general SHARE menu. At least not so far … :) The bookmarklet does nothing more as sending the link with some additional information like name (title) to the URL https://trello.com/add-card, where the user can select the desired board and list: Trello-AddCard-Bookmarklet.js1234567891011121314151617javascript: (function (win, name, desc) { win.open( "https://trello.com/add-card" + "?source=" + win.location.host + "&mode=popup" + "&url=" + encodeURIComponent(win.location.href) + (name ? "&name=" + encodeURIComponent(name) : "") + (desc ? "&desc=" + encodeURIComponent(desc) : ""), "add-trello-card", "width=500,height=600,left=" + (win.screenX + (win.outerWidth - 500) / 2) + ",top=" + (win.screenY + (win.outerHeight - 740) / 2) );})(window, document.title, getSelection ? getSelection().toString() : ""); If you want to use this bookmarklet, you have to remove all line breaks from the code, decode the url and save it as a bookmark … or you visit https://trello.com/add-card and drag the link “Send to Trello” to your bookmark list. It comes out the same. As I said above, the Trello Bookmarklet e.g. Add Card page works differently to the Android app from the same company in terms of the URL. Are different departments not talking to each other? However … for me, the bookmarklet is history, because there is a much better approach that uses the Add Card page also, but can be called up from the standard Android Share Menu. Power Tool: HTTP ShortcutsThe trigger for my new solution was my search for better interaction possibilities from my blog to the IndieWeb. One hit was the article Android IndieWeb interactions with the HTTP Shortcuts app by Ryan Barrett, which uses the app HTTP Shortcuts by Roland Meyer to serve as a Share Target for LIKE, FOLLOW and REPLY actions. Share Target? A while ago I had the idea of using the Web Share Target API to solve my problem above, but haven’t got round to setting up such a PWA yet. But I no longer have to do that, because the HTTP Shortcuts app not only offers me the option of serving as a share target, but also significantly more functions for creating Android shortcuts, which are extremely useful: Browser Shortcut - Open the URL in the browser Scripting Shortcut - Write JavaScript for advanced workflows Multi-Shortcut - Trigger multiple shortcuts at once All these shortcuts can be used to send any HTTP requests and process the responses in a variety of ways. The feature list is impressive and well documented. Scripting Shortcut for calling Trello’s Add Card PageFor my case, I need a scripting shortcut to process the information shared by the Android sharing dialogue to call the Add Card page. This is done via static variables that are created in HTTP Shortcuts and can then be used later in the script: Creating the shortcut is quite simple, as you can see from the screenshots. Select the type, assign a name, perhaps a suitable icon and (importantly) tick the option Show as app shortcut on launcher and promote as Direct Share target, which ensures that the shortcut appears in the Android Share menu. The script is no less straightforward, because it simply assembles the URL to the Trello Add Card page with the static variables and opens it using the app’s built-in JavaScript function openUrl: 123456789101112131415const url = getVariable("SharedUrl");const title = getVariable("SharedTitle");const getHostnameFromRegex = (url) => { const matches = url.match(/^https?\\:\\/\\/([^\\/?#]+)(?:[\\/?#]|$)/i); return matches && matches[1];}const hostname = getHostnameFromRegex(url);const trelloAddUrl = "https://trello.com/add-card?source=" + hostname + "&mode=popup&url=" + encodeURIComponent(url) + "&name=" + encodeURIComponent(title);openUrl(trelloAddUrl); ConclusionThe result is convincing and frees me from the bookmarklet on my smartphone, but I will continue to use it on my desktop browser. Download Shortcut Files for Import in HTTP Shortcut AddToTrello-Shortcut.zip The HTTP Shortcuts app, however, has really got me hooked. I see a lot of potential for automating things on my smartphone and will keep you updated as I develop more solutions with it. Thank you Roland for this gem …","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Trello","slug":"Trello","permalink":"https://kiko.io/tags/Trello/"},{"name":"Android","slug":"Android","permalink":"https://kiko.io/tags/Android/"}]},{"title":"Experimenting with the font LEXEND","subtitle":"Easier reading for users and more beautiful typography","date":"2023-11-12","updated":"2023-11-12","path":"post/Experimenting-with-the-font-LEXEND/","permalink":"https://kiko.io/post/Experimenting-with-the-font-LEXEND/","excerpt":"A few weeks ago, a blog entry (I can’t remember which one) drew my attention to a font called LEXEND. In this article, the author also went into the scientific background of the font, which was developed to simplify reading and thus support those with reading difficulties. The website lexend.com, operated by The Lexend Group, therefore also advertises the font with all kinds of reading statistics, although it is open source and is also freely available via Google Fonts. I simply liked the font style and wanted to try it out on this blog, which has always used the Open Sans font. It was harder than I thought…","keywords":"weeks ago blog entry remember drew attention font called lexend article author scientific background developed simplify reading support difficulties website lexendcom operated group advertises kinds statistics open source freely google fonts simply style wanted sans harder thought…","text":"A few weeks ago, a blog entry (I can’t remember which one) drew my attention to a font called LEXEND. In this article, the author also went into the scientific background of the font, which was developed to simplify reading and thus support those with reading difficulties. The website lexend.com, operated by The Lexend Group, therefore also advertises the font with all kinds of reading statistics, although it is open source and is also freely available via Google Fonts. I simply liked the font style and wanted to try it out on this blog, which has always used the Open Sans font. It was harder than I thought… The Download OdysseyAt the bottom of the one-pager website lexend.com there is a form for the first name and e-mail address and a button “Subscribe & Send me these Fonts”. Okay … Seriously? Fuel for the spam machine? But my spam filter seems to be good enough and so I had the mail sent to me, only to be offered a file called readexpro-master.zip for download via the tracker button in the mail called “Download the Latest Fonts”. Again … Seriously? I expect the Lexend font files and I get something else, without any hint or description? How quickly do the people at The Lexend Group want to destroy their reputation on the net? Where was the UNSUBSCRIBE link in the mail again?But of course I could have used the less prominent link to Google Fonts below the sample image in the mail, which actually leads to the expected font. But what the heck is ReadExPro? Research… The font was designed in 2018 by Bonnie Shaver-Troup and Thomas Jockin and is based on the Quicksand project by Andrew Paglinawan, which was initiated in 2008 and improved by Thomas Jockin for Google Fonts in 2016. However, lexend.com only mentions Shaver-Troup and her team, who started working on Lexend with Google in 2017. In 2021, Thomas Jockin forked the project and worked with Nadine Chahine on an expansion for Arabic and renamed the font Readex Pro. Ah … ok. And why doesn’t anyone tell you that on the Lexend site? So Lexend is only one half of the coin and Readex Pro makes it a whole? Why two names for it? And why do you get one when you expect the other? Mysterious and, in times of all kinds of dangers on the Internet, simply bordering on the dubious. But unfortunately this is not the end of the inconsistencies. Since the files in my download of ReadEx Pro are quite large and I have no need for Arabic characters, I downloaded the Lexend font package from Google Fonts, which ONLY contains TTF files and no WOFF2 (Webfonts), the compressed version. Google itself writes the following in its own Glossary of Web Fonts, but then refrains from supplying them? Another: Seriously? WOFF (and its successor WOFF2) are compressed file formats created specifically for web fonts. Although regular OpenType fonts (TTF and OTF files) can be used as web fonts, such usage is not recommended as it usually contravenes licence agreements-and the files are significantly larger. However, if you embed the font directly from Google Fonts instead of downloading it, only WOFF2 is delivered and TTF is out of the picture. It may be a consistent approach by Google to offer as much as possible only online in order to be able to better analyse the traffic behind it, but especially in Germany, where a wave of warnings has swept over website operators in the last two years precisely because of this data collection via Google Fonts, this leaves a bitter aftertaste and forces developers to do additional work where it would not be necessary. Convert TTF to WOFF2A short detour … The Web Open Font Format (WOFF) has been around since 2012. The second version (WOFF2), in which the extremely efficient Brotli compression method is used, was drafted in 2014. All browsers support WOFF2 nowadays, so there is no longer any need to include WOFF, as is often still the case. The same applies to other historical and dead formats EOT and SVG. So if you have a font as TTF and want to use it on the web, you can either use the open source compression from Google on the command line or you can use a free online compressor such as the one from Yabe Webfonts. To include the font in your CSS, you ONLY need the following code for modern browsers. If you have to support old favorites like IE 8 or similar, you have completely different problems anyway and should think about changing jobs. 1234@font-face { font-family: 'MyWebFont'; src: url('myfont.woff2') format('woff2');} But back to Lexend and a download that works. All fonts on Google Fonts can also be found on GitHub and so there is also a repository for Lexend: googlefonts/lexend. In it you will find both TTF and WOFF2 files, at least for the standard font Lexend, although not for the variants (Deca, Lexa, Giga etc.), which I don’t need for my part anyway. 1234567891011121314151617181920212223242526\\lexend-main\\fonts\\lexend+---ttf Lexend-Black.ttf Lexend-Bold.ttf Lexend-ExtraBold.ttf Lexend-ExtraLight.ttf Lexend-Light.ttf Lexend-Medium.ttf Lexend-Regular.ttf Lexend-SemiBold.ttf Lexend-Thin.ttf +---variable Lexend[HEXP,wght].ttf +---webfonts Lexend-Black.woff2 Lexend-Bold.woff2 Lexend-ExtraBold.woff2 Lexend-ExtraLight.woff2 Lexend-Light.woff2 Lexend-Medium.woff2 Lexend-Regular.woff2 Lexend-SemiBold.woff2 Lexend-Thin.woff2 You might have noticed in the files list, that there are no italic or oblique font faces. There is a reason for this, because there are currently none for Lexend. Although there is a corresponding GitHub issue (Update Lexend with Italics), but it hasn’t really been going anywhere for over a year. Richard Hriech did publish a cursive variant called LexendItalic last year, but the project looks more like a quick fix created within a day and therefore I haven’t tried it out.However, the absence does not bother me, because according to the W3C, the browser mimics the so-called sloping effect if it cannot find an appropriate font. I think it does this quite well and saves me having to work with additional font files. The Integration MadnessLet’s get straight to the point: My difficulties in using Lexend here on kiko.io were entirely homemade. Integrating the font made me realize all the CSS mistakes I’ve made in recent years, because Lexend has a significantly different character spacing than OpenSans I’ve been using so far. It has never been a good idea to lean too heavily on typography when designing layouts. The careless mixing of REM and PX was also my downfall. Everything looked pretty shitty … and gave me the incentive to refactor the entire CSS (or Stylus), regardless of whether I would continue to use Lexend or not. After many days of standardising, dismantling, recalculating, rewriting and also throwing away, the site had a completely new and better look for me, although (mostly) nothing had changed in the basic structure. The Lexend font, which I find much more suitable, also contributed to this and so I will leave it like that. Whether you can now read my posts faster, as the font promises, is for others to judge. Toggle to Open Sans let bFontToggle = false; function fontToggle() { if (bFontToggle === false) { document.getElementById(\"body\").style.fontFamily = \"Open Sans\"; document.getElementById(\"body\").style.fontWeight = 400; document.getElementById(\"fontToggle\").textContent = \"Reset to Lexend\"; } else { window.location.reload(); } bFontToggle = !bFontToggle; } More Info lexend.com Lexend Italic Version How to Convert a TTF Variable Font to WOFF2 for Improved Web Performance How To Convert Variable TTF Font Files To WOFF2","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Theming","slug":"Theming","permalink":"https://kiko.io/tags/Theming/"}]},{"title":"SVWW vs. Hansa Rostock @ 2023-10-29","subtitle":"Another step to class retention","series":"SV Wehen Wiesbaden","date":"2023-10-30","updated":"2023-10-30","path":"post/SVWW-vs-Hansa-Rostock-2023-10-29/","permalink":"https://kiko.io/post/SVWW-vs-Hansa-Rostock-2023-10-29/","excerpt":"1:0 Football again. After my little excursion into the Europa Conference League, my team from SV Wehen Wiesbaden was called upon again on Sunday and with them the fans in the stadium. On the one hand because it was raining (and that is sometimes no fun in the front row) and on the other hand because the boys had won the last away game in Osnabrück and now had the opportunity to temporarily establish themselves in the midfield of the 2. Bundesliga. Another win was needed against an opponent from Rostock who, after a good start at the beginning of the season, had lost the last 6 games! Since I have no chance to park my car anywhere at the stadium, I have already planned before the season to always go there with the scooter, which is a pure pleasure in the summer. With 10 degrees and light rain it was diemal but rather suboptimal conditions. But anyway… I had expected that a lot of police, water cannon and other executive equipment would be on site, as it was already the case at the Schalke game, because the Hansa fans have a pretty bad reputation … But no. Not particularly many Rostockers have taken on the 700 kilometer journey and so the opposing stand was only half full and the police relaxed.","keywords":"football excursion europa conference league team sv wehen wiesbaden called sunday fans stadium hand raining fun front row boys won game osnabrück opportunity temporarily establish midfield bundesliga win needed opponent rostock good start beginning season lost games chance park car planned scooter pure pleasure summer degrees light rain diemal suboptimal conditions anyway… expected lot police water cannon executive equipment site case schalke hansa pretty bad reputation … rostockers kilometer journey opposing stand half full relaxed","text":"1:0 Football again. After my little excursion into the Europa Conference League, my team from SV Wehen Wiesbaden was called upon again on Sunday and with them the fans in the stadium. On the one hand because it was raining (and that is sometimes no fun in the front row) and on the other hand because the boys had won the last away game in Osnabrück and now had the opportunity to temporarily establish themselves in the midfield of the 2. Bundesliga. Another win was needed against an opponent from Rostock who, after a good start at the beginning of the season, had lost the last 6 games! Since I have no chance to park my car anywhere at the stadium, I have already planned before the season to always go there with the scooter, which is a pure pleasure in the summer. With 10 degrees and light rain it was diemal but rather suboptimal conditions. But anyway… I had expected that a lot of police, water cannon and other executive equipment would be on site, as it was already the case at the Schalke game, because the Hansa fans have a pretty bad reputation … But no. Not particularly many Rostockers have taken on the 700 kilometer journey and so the opposing stand was only half full and the police relaxed. The nice old lady to my left (I really have to ask her about her name) was also there again, after missing the last home game due to a wedding, as she told me. The silent one on my right also and also this time he didn’t speak a word, so I’m starting to wonder if he can talk at all. The howler monkeys behind me were of course also there again and they had in the course of the game again plenty of opportunity to insult the referee, opposing players or our coach in the worst way. This time there were only 8,600 spectators, which was probably also due to the weather. It was just cold and wet and I felt a bit sorry for the players, especially the goalkeepers. How do you keep warm when you’re standing on the field in shorts and not constantly running back and forth? A football mystery. The GameIn the first 30 minutes the game was quite even. You could tell that both teams wanted a win, but the goal attempts, especially from Rostock, were nothing more than puny. Ours did a little better, but the ball still always went past the goal. Both teams were also a bit aggressive, for example, Perea from Rostock annoyed our keeper Stritzel so much that he extended his elbow and Perea sank to the ground like a striving man and rolled back and forth theatrically. An undignified spectacle, which the fans acknowledged with shrill whistles … and the referee with a penalty! Stritzel has saved fantastic balls this season, but not a penalty yet … until then, as he parried Kinsombi’s weak shot to the side. Yesss…! Nothing much happened until half-time, but our guys were slowly gaining the upper hand, as the game was played almost exclusively in the opponent’s half at the beginning of the second half, with occasional counter-attacks by Rostock, but they were too erratic to have any effect. Our defence stood firm. let macy_empr3t = new Macy({ container: '#image-masonry-empr3t', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); The increasing control of the game also led to more chances to score, but either it went just over or Rostock keeper Markus Kolke (who played a total of 8 years in Wiesbaden until 2019) fished it out of the corners. The closer we got to the end of the game, the more nervous the fans (or just me and my seatmate) got. OMG, that thing has to go in, now! The redemption came in the 89th minute: After a standard, Kolke parried Fechner’s shot forward and Prtajin only had to stick his head out … 1:0! ⇾ Match report on kicker.de ConclusionIt was a nerve-wracking, but in the end a happy game. We fans would like to see more of these games, especially if it ends in a win. I haven’t seen our boys lose since I started going to the stadium thsi season. We can keep it up :D","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"},{"name":"2. Bundesliga","slug":"2-Bundesliga","permalink":"https://kiko.io/tags/2-Bundesliga/"}]},{"title":"SGE vs. HJK @ 2023-10-26","subtitle":"Europa Conference League: Eintracht Frankfurt against HJK Helsinki","date":"2023-10-29","updated":"2023-10-29","path":"post/SGE-vs-HJK-2023-10-26/","permalink":"https://kiko.io/post/SGE-vs-HJK-2023-10-26/","excerpt":"6:0 My newly awakened love for German soccer reached a small peak last Thursday when my colleague Uli asked me, if I would like to go to the evening match of his favorite club Eintracht Frankfurt against HJK Helsinki as part of the preliminary round of the Europa Conference League. He had come by a happy coincidence to two tickets. Of course! Let’s go! Now I’m not an Eintracht fan, by any means. In the Rhine-Main area, the Frankfurt club is either hated or loved by the people, because its fan base is a bit more aggressive, i.e. they are disreputable and with them the club. But I’ve never been to the Waldstadion (as it used to be called) or the “Deutsche Bank Park” (as it’s called if you put money on the table), one of the big soccer arenas in Germany, where international matches and other big events are also held regularly.","keywords":"newly awakened love german soccer reached small peak thursday colleague uli asked evening match favorite club eintracht frankfurt hjk helsinki part preliminary round europa conference league happy coincidence tickets lets im fan means rhine-main area hated loved people base bit aggressive disreputable ive waldstadion called deutsche bank park put money table big arenas germany international matches events held regularly","text":"6:0 My newly awakened love for German soccer reached a small peak last Thursday when my colleague Uli asked me, if I would like to go to the evening match of his favorite club Eintracht Frankfurt against HJK Helsinki as part of the preliminary round of the Europa Conference League. He had come by a happy coincidence to two tickets. Of course! Let’s go! Now I’m not an Eintracht fan, by any means. In the Rhine-Main area, the Frankfurt club is either hated or loved by the people, because its fan base is a bit more aggressive, i.e. they are disreputable and with them the club. But I’ve never been to the Waldstadion (as it used to be called) or the “Deutsche Bank Park” (as it’s called if you put money on the table), one of the big soccer arenas in Germany, where international matches and other big events are also held regularly. Getting to the stadium was an ordeal, because we had left our cars at the office and taken the cab, but the 55,500 spectators all seemed to want to park right in front of the arena so the traffic jam was immense. The last kilometer through the forest to the stadium and the Jürgen Grabowski grandstand to our seats were tough, at least for Uli, because his knee was hurting as usual. But the anticipation, a beer and an Äppler made up for everything. What I found amazing, as a Waldstadion newbie, was that we entered the building at ground level, but came out in row 30. The whole thing thus reminded of a nicely designed giant pit. The lighting of this massive area was perfect and the volume of the fans was loud, but not deafening. An architectural masterpiece. The wall of fans in the home curve with the many oversized flags that were waved throughout was impressive, even if I wondered the whole time, how the fans behind these giant flags would have noticed anything of the game for even a minute. But I think it was not about free view of the people there at all. Only about the atmosphere. I was in any case happy about my seat in row 9 pretty directly on the center line of the field and had to ask the people in the row of seats in front of me once in a while to sit down again so that I could see the game comfortably. … vs … (cute) The GameThe first 10 minutes of the game were unspectacular. They felt each other out and the Finns played diligently and should have been in the lead after 5 minutes, but got in the 9th minute after some guesswork from the referee with VAR support a penalty against them. let macy_ydg4i0 = new Macy({ container: '#image-masonry-ydg4i0', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); After this 1:0 for Eintracht, the Frankfurt boys whirled themselves into a kind of frenzy of play. More and more fluidly and casually it went in the direction of the Finnish goal and Helsinki could only with effort hold against it. So it was after 30 minutes already 3:0 and at halftime 4:0. The goal to make it 3-0 was a really spectacular one: Omar Marmoush gets the ball in the penalty area and takes a shot, but the ball bounces off an opponent’s legs and comes back to him. He pulls off again and again the ball bounces off the legs of another player back to him. His third attempt was then a fine shot with the outside of his foot past all the players into the goal. In the break, the coach of the Finns seems to have been a little louder, because his guys played a little stronger and more determined, only to get another one in the 55th minute. With 10 minutes to go, things got emotional in the stadium: the coach substituted Timothy Chandler, a Frankfurt veteran who has been with the club for almost 10 years and his first appearance this season. The tens of thousands of fans were completely out of their minds, screaming “Tiiiimmmyyy” as soon as he got to the ball. It was crazy. How must it feel to be showered with so much fan love! Things almost got out of hand when Chandler sprinted forward on the right flank in the 89th minute and made a wonderful pass into the middle and Dina Ebimbe just had to slot it in for 6:0. let macy_2l3use = new Macy({ container: '#image-masonry-2l3use', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); ⇾ Match report on kicker.de ConclusionIt was really an experience to see this game in this arena and even if I would have enjoyed Helsinki sending Frankfurt home 4:0 (my funny tip to Uli before the game), it was nice that he could be happy about the victory.","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"Eintracht","slug":"Eintracht","permalink":"https://kiko.io/tags/Eintracht/"}]},{"title":"Handling IPTC metadata on Android and Windows","subtitle":null,"date":"2023-10-28","updated":"2023-10-28","path":"post/Handling-IPTC-metadata-on-Android-and-Windows/","permalink":"https://kiko.io/post/Handling-IPTC-metadata-on-Android-and-Windows/","excerpt":"Most of the photos that I prepare for this website for example, I take with my big Nikon camera in RAW format and then edit them under Windows in Adobe Lightroom Classic. I always give the image a name and a few keywords, which are then also displayed on the photo page. Lightroom stores these as IPTC metadata in the sidecar file and writes them, when exporting the image, directly into the header of the generated JPG file. Now, however, I take pictures on the go with my Pixel smartphone and process them with the Google Photos Android app, and although the IPTC standard is only 30 years old, Google doesn’t support it! You can give the image a “description” but it is not written into the image as a metadata, but is probably lying around somewhere on the Google servers.","keywords":"photos prepare website big nikon camera raw format edit windows adobe lightroom classic give image keywords displayed photo page stores iptc metadata sidecar file writes exporting directly header generated jpg pictures pixel smartphone process google android app standard years doesnt support description written lying servers","text":"Most of the photos that I prepare for this website for example, I take with my big Nikon camera in RAW format and then edit them under Windows in Adobe Lightroom Classic. I always give the image a name and a few keywords, which are then also displayed on the photo page. Lightroom stores these as IPTC metadata in the sidecar file and writes them, when exporting the image, directly into the header of the generated JPG file. Now, however, I take pictures on the go with my Pixel smartphone and process them with the Google Photos Android app, and although the IPTC standard is only 30 years old, Google doesn’t support it! You can give the image a “description” but it is not written into the image as a metadata, but is probably lying around somewhere on the Google servers. Downloading the image via Google Photos and giving it a name and a few keywords using on-board tools under Windows doesn’t work either. There is the display of metadata in File Explorer under PROPERTIES and DETAILS, but editing TITLE or DESCRIPTION does not lead to IPTC metadata either, because these are not supported by Microsoft. Now you can ask yourself why IPTC is not important to the big companies, especially since it doesn’t seem to be the problem technically, as Adobe and other companies prove, but so what … I need a solution for my problem. Android SolutionFor Android, there is probably no better image metadata app than EXIF Pro. Besides the GALLERY, which lists all images on the smartphone, it also has a browser to load other images and it supports all metadata formats, including IPTC. All metadata can be edited, added or deleted if you want to save space, for example. In my case, I just have to select the image in the app and use ADD in the IPTC section to select the ObjectName (for the name) and Keywords (for the keywords), enter the values and save the file. Done. The app is based on the platform-independent Perl library ExifTool by Phil Harvey … and as often as you research for ways to manipulate image metadata and whatever platform on the net, you will always come across Phil’s tool. It is the gold standard in this area, so to speak. The list of supported formats is more than impressive and that of meta formats leaves nothing to be desired. Windows SolutionMy problem described above is actually already solved with the Android solution, but under Windows there is this IPTC gap too and it can be filled with … what do you think … the ExifTool from Phil Harvey ;) First of all, ExifTool is a pure command line tool and also comes without an installer or anything like that. For Windows there is a ZIP download with a single file inside: exiftool(-k).exe. You save it in a folder of your choice and drag & drop an image file onto it and the metadata will be displayed in a terminal window. “-k” in the name means that ExifTool is called with this parameter, which makes sure that the terminal window stays open after processing. For use in the command line it is best to copy the file and rename it to exiftool.exe or you can download the installer variant by Oliver Betz, which also makes a PATH entry to be able to call it from anywhere. To add the name and the keywords to a file, the following call is sufficient: (lines wrapped for readability)12345exiftool -overwrite_original -iptc:ObjectName="My New Name" -iptc:Keywords="keyword1,keyword2,keyword3" "C:\\Pictures\\MyImage.jpg" Okay … You don’t buy Windows to use the command line. There is also a GUI called ExifToolGUI for Windows v5.xx by Bogdan Hrastnik. It is already more than 10 years old and also comes without an installer, but it works great … if you have understood the somewhat strange concept of workspaces and adding new fields to them, or if you have somehow muddled your way through. ConclusionI wish IPTC would not only be supported by the handful of programs, but like the technical meta format EXIF would become a standard in every image processing software, especially in the Big Five, who neglect it so far. But maybe my contribution here helped you to deal with it.","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Android","slug":"Android","permalink":"https://kiko.io/tags/Android/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"},{"name":"Metadata","slug":"Metadata","permalink":"https://kiko.io/tags/Metadata/"}]},{"title":"Get and use a dominant color that matches the header image","subtitle":"Vibrant Color library and recalculating its HSL output values","date":"2023-10-10","updated":"2023-10-10","path":"post/Get-and-use-a-dominant-color-that-matches-the-header-image/","permalink":"https://kiko.io/post/Get-and-use-a-dominant-color-that-matches-the-header-image/","excerpt":"The pages of this website are all structured according to the scheme Header, Content, Footer, where the header contains an individual hero image and, like the footer, has a dark gray background color. I chose dark gray at the time because I needed contrast and it matches all the other colors, since the header image is slowly overlaid by the header background color as you scroll. In July last year, in Discoveries #19 - Visual Helpers, I introduced two tools that deal with determining the dominant color from an image, and since then I’ve been buzzing around in my head about using one of them to color the header and footer to match the image. After a short testing period I decided to use Vibrant Colors (also because of the good example by Konstantin Polunin on Codepen) and how it works and how I had to adapt the results to my needs I want to highlight in this post.","keywords":"pages website structured scheme header content footer individual hero image dark gray background color chose time needed contrast matches colors slowly overlaid scroll july year discoveries #19 - visual helpers introduced tools deal determining dominant ive buzzing head match short testing period decided vibrant good konstantin polunin codepen works adapt results highlight post","text":"The pages of this website are all structured according to the scheme Header, Content, Footer, where the header contains an individual hero image and, like the footer, has a dark gray background color. I chose dark gray at the time because I needed contrast and it matches all the other colors, since the header image is slowly overlaid by the header background color as you scroll. In July last year, in Discoveries #19 - Visual Helpers, I introduced two tools that deal with determining the dominant color from an image, and since then I’ve been buzzing around in my head about using one of them to color the header and footer to match the image. After a short testing period I decided to use Vibrant Colors (also because of the good example by Konstantin Polunin on Codepen) and how it works and how I had to adapt the results to my needs I want to highlight in this post. The work on the project node-vibrant, is probably based on vibrant.js (at least this archived repo refers to it) and is actually a Node.js solution, but you can also just use it in the browser. Since the determination of the total of 6 different colors (Vibrant, Muted and both with its variants of Light und Dark mode) from an image can possibly lead to performance problems in the browser, the maintainers have focused on Node.js and also built in a WebWorker solution. My approach is based on the already fully loaded page, including the header image and the display with the original dark gray background color. So the new color is simply icing and thus should not lead to problems. Here is an example of the 6 extracted colors with one of my images from Konstantine’s codepen, mentioned above: SetupThe installation is done quickly … Execute npm install node-vibrant in the console, copy out the file vibrant.min.js into the distribution script folder and include it into the HTML page header: 1<script src="/js/dist/vibrant.min.js"></script> PreparationDepending on the size of the image to be read out, the determination may take some time. Therefore, it is advisable, as in my case, not only to compress the images, but also to let them preload in the head of the page. For this you can use the rel=preload mechanism, which ensures that a resource is loaded fast. The preload value of the element’s rel attribute lets you declare fetch requests in the HTML’s , specifying resources that your page will need very soon, which you want to start loading early in the page lifecycle, before browsers’ main rendering machinery kicks in. --- MDN Web Docs By adding an ID to the LINK it will be easy later on to get the URL of the image to throw at the library: 1234<link rel="preload" as="image" id="photo-preload" href="/photos/normal/20-08-Mallorca-7333.jpg" imagesrcset="/photos/mobile/20-08-Mallorca-7333.jpg 480w, /photos/tablet/20-08-Mallorca-7333.jpg 768w"> Usage, First AttemptThus prepared, it is easy to read out the preferred RGB color value in a script that is embedded in the footer of the page, using Vibrant Color: after-footer.js12345678910111213141516171819202122232425function setVibrantColor() { // get url of preloaded image let img = $("#photo-preload").attr("href"); // call Vibrant Color Vibrant.from(img).getPalette().then(palette => { // get vibrant color swatch let swatch = palette.Vibrant; // helper to format output const _rgbToString = (_rgb) => `rgb(${_rgb[0]}, ${_rgb[1]}, ${_rgb[2]})`; // get formatted RGB value of swatch let rgb = _rgbToString(swatch.getRgb()); // set new background color to header and footer $("#header, #footer").css("background-color", rgb); });}$(document).ready(function() { setVibrantColor();}); The MUTED variants were out of the question for me after a bit of trial and error, because for me this color has too little to do with what the eye immediately draws out of the image. This can be seen in the example above: the green of the MUTED colors is by no means dominant. That’s why I decided to use VIBRANT. Usage, Second AttemptBut there is also a problem with the VIBRANT variant: under certain circumstances the extracted color is too bright for the background with white text in the foreground. Vibrant Color offers the possibility to create matching text colors, but neither worked for me, nor did I want to get away from white. If you additionally offer a dark theme like I do, the selected color will definitely be too light. Besides the possibility to read out the RGB color from the so-called swatches, the library also offers the output HSL (Hue/Saturation/Lightness). By adjusting the L-value downwards, the desired contrast can be achieved again. The HSL values in Vibrant Colors are only available as decimal numbers, i.e. it is necessary to convert them using the HSL specification: after-footer.js12345678910111213141516171819202122232425262728293031function setVibrantColor(theme) { // get url of preloaded image let img = $("#photo-preload").attr("href"); // call Vibrant Color Vibrant.from(img).getPalette().then(palette => { // get vibrant color swatch let swatch = palette.Vibrant; // get HSL from swatch and convert values let _hsl = swatch.getHsl(); let h = (_hsl[0] * 360); // in relation to the HSL color wheel let s = (_hsl[1] * 100); // percentage let l = (_hsl[2] * 100); // percentage // overwrite l with new fixed hue depending on theme l = (theme === "dark") ? 20 : 35; // helper to format output, limited to 4 decimal places const _hslToString = (h, s, l) => `hsl(${h.toFixed(4)}, ${s.toFixed(4)}%, ${l.toFixed(4)}%)`; // get formatted HSL value let hsl = _hslToString(h, s, l); // set new background color to header and footer $("#header, #footer").css("background-color", hsl); });} Usage, Final AttemptHSL generally has the problem that colors appear brighter than others depending on the saturation. Therefore, the HSV color model is often preferred, because it corresponds more closely to human color perception. A complete conversion to HSV seemed a bit too much for my case. Therefore, I limited myself to downscaling the lightness proportionally from a saturation value of 70%: after-footer.js1234567891011121314function setVibrantColor(theme) { ... // overwrite l with new fixed hue depending on theme l = (theme === "dark") ? 20 : 35; // calculate hue reduction when saturation too high let reduce = (s > 70) ? 10 - ((100 - s) / 4) : 0; // take half of reduction when dark theme l -= (theme === "dark") ? reduce / 2 : reduce; ...} The ResultThe result convinces me, even if it is not (yet) perfect. But that is primarily due to the choice of the dominant color of the library. According to my color perception, it could be different from time to time … like for example for the page of this post. My eye would have expected some kind of violet. But on other pages it works fine, even the reduced lightness, because my header image at the top is darkened too with a gradient to let the text pop out more.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"},{"name":"Theming","slug":"Theming","permalink":"https://kiko.io/tags/Theming/"}]},{"title":"SVWW vs. HSV @ 2023-10-07","subtitle":"Two hearts beat in my chest","series":"SV Wehen Wiesbaden","date":"2023-10-07","updated":"2023-10-07","path":"post/SVWW-vs-HSV-2023-10-07/","permalink":"https://kiko.io/post/SVWW-vs-HSV-2023-10-07/","excerpt":"1:1 Since my youth I am a (small) fan of the Hamburger Sportverein (HSV). Magath, Hrubesch, Keegan were my sporting heroes back then. This was certainly also related to the fact that the club was one of the best in Europe in the early 80s. Two championships, European Cup victory and a strong team left an impression on me. As the last founding member of the 1. Bundesliga, however, it was always in danger of relegation in the last 10 years and then really relegated to the 2. Bundesliga in 2018 after 55 years. What followed were sad attempts to climb back up. Always so close that the saying prevailed: “How can you tell that it’s springtime in Germany? The trees are sprouting and HSV is fucking up the promotion!” In my first post in this series, I described how my hometown club from Wiesbaden made it into the 2. Bundeliga and this particular home game this weekend has a special appeal for me, of course. The favorite club from my youth against the one from my present. My Wehen Wiesbaden against my HSV.","keywords":"youth small fan hamburger sportverein hsv magath hrubesch keegan sporting heroes back related fact club europe early 80s championships european cup victory strong team left impression founding member bundesliga danger relegation years relegated sad attempts climb close prevailed springtime germany trees sprouting fucking promotion post series hometown wiesbaden made bundeliga home game weekend special appeal favorite present wehen","text":"1:1 Since my youth I am a (small) fan of the Hamburger Sportverein (HSV). Magath, Hrubesch, Keegan were my sporting heroes back then. This was certainly also related to the fact that the club was one of the best in Europe in the early 80s. Two championships, European Cup victory and a strong team left an impression on me. As the last founding member of the 1. Bundesliga, however, it was always in danger of relegation in the last 10 years and then really relegated to the 2. Bundesliga in 2018 after 55 years. What followed were sad attempts to climb back up. Always so close that the saying prevailed: “How can you tell that it’s springtime in Germany? The trees are sprouting and HSV is fucking up the promotion!” In my first post in this series, I described how my hometown club from Wiesbaden made it into the 2. Bundeliga and this particular home game this weekend has a special appeal for me, of course. The favorite club from my youth against the one from my present. My Wehen Wiesbaden against my HSV. Unfortunately I missed the game two weeks ago against Elversberg, because I had already made a promise to go to Mannheim to the Bundesgartenschau, but maybe it was a good thing, because my team was in a bad mood and lost 0:2. Just like the last away game against Hannover (2:0) and the cup game against RB Leipzig at home in between (2:3), where I actually wanted to go, but due to a small error in understanding did not get into the stadium. So it was time to go back to the arena, because with me SVWW always had something to cheer about, even if it was just a draw that felt like a win. I also have a few HSV fans in my circle of friends and so I made use of my right of first refusal of 4 more tickets for this game. I sat on my permanent seat in the front row and my friends at a few rows behind me. The elderly lady on my left was not present this time, but a nice man who had taken her place was. The silent one on my right did not come at all and his seat thus remained empty. The GameHSV is actually in shape this year to make it back to the Bundesliga. They have already dropped a few points, but have been on the promotion places from the beginning. This year it should be something and accordingly dominant have the Hamburg the game against once again deep standing Wiesbadener also started. Very sure of the ball and winning almost every duel, they were unable to capitalize on this in the first half. They brought the ball very close to the goal, but not into it, and you could tell that the guests were getting a little more annoyed as time went on. This was expressed shortly before the half-time break also in a few unsportsmanlike conduct. Dropping and claiming foul play is simply stupid and only provokes catcalls. The one or other time is overall bad referee but also fell for the trick or has made other nonsensical decisions, which caused the “right” fans around me to nastiest insults. let macy_9to8wg = new Macy({ container: '#image-masonry-9to8wg', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); After the break, the game continued just as it had stopped a quarter of an hour earlier: HSV played beautifully, safely but ineffectively. They even scored a goal, but it was disallowed for offside. The SVWW was defending with all men in their own half, sometimes desperately, and only rarely managed to counterattack. 9 minutes before the end, the Wiesbadener were then once in front of the opposing goal and from a rather harmless header and a weak defense of the goalkeeper became a goal! 1:0! Unbelievable! The remaining 9 minutes plus 6 minutes of injury time can be called spectacular: HSV ran frantically and tried everything to put the f***ing ball in the goal and in the 87th minute the ball was actually in our net … 1:1! But that was not enough for the guests. They kept increasing the pressure to win this game. Partly too hectic and headless, but we had real trouble to fend them off. However, one of these defensive actions in the penalty area led to a penalty kick for HSV in the 97th minute. Damn! No! … but the penalty taker slammed the thing against the crossbar! YESSS! ⇾ Match report on kicker.de ConclusionAnother one of those draws that feels like a win. It’s a pity that it wasn’t enough for a win, but you have to admit that HSV was really a class above. They’re really playing for promotion and we’re playing to stay in the league. What gives me courage is that the Wiesbaden team fights so passionately and never gives up. Every opponent has a very hard time with this defense and that’s a good approach to still play in the 2nd Bundesliga next year.","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"},{"name":"2. Bundesliga","slug":"2-Bundesliga","permalink":"https://kiko.io/tags/2-Bundesliga/"}]},{"title":"Majorcan Details","subtitle":null,"series":"New Photos","date":"2023-10-03","updated":"2023-10-03","path":"post/Majorcan-Details/","permalink":"https://kiko.io/post/Majorcan-Details/","excerpt":"Summer is over and I’m spending some of my free time post-processing in Lightroom the many photos I’ve taken over the past few months. As a casual photographer, I mostly use my vacation and weekend trips to take my Nikon for a walk and let its chip card glow. My better half is constantly wondering why I stopped there or here again, even if she likes the results afterwards. The point is that I am a color-and-shape junkie. Of course I also do landscapes and portraits, but the details of things have done it to me the most. Colors & Shapes … In July I was for a few days with friends on a finca in the south of Mallorca and of course I used every opportunity to stick my lens everywhere where there were interesting details to be expected. I don’t know if it had anything to do with the heat, but most of the motifs have a very earthy touch. I probably wanted to stay somewhere underground all the time, because 40 degrees Celsius in the shade was then also a bit too much for me.","keywords":"summer im spending free time post-processing lightroom photos ive past months casual photographer vacation weekend trips nikon walk chip card glow half constantly wondering stopped likes results point color-and-shape junkie landscapes portraits details things colors & shapes … july days friends finca south mallorca opportunity stick lens interesting expected dont heat motifs earthy touch wanted stay underground degrees celsius shade bit","text":"Summer is over and I’m spending some of my free time post-processing in Lightroom the many photos I’ve taken over the past few months. As a casual photographer, I mostly use my vacation and weekend trips to take my Nikon for a walk and let its chip card glow. My better half is constantly wondering why I stopped there or here again, even if she likes the results afterwards. The point is that I am a color-and-shape junkie. Of course I also do landscapes and portraits, but the details of things have done it to me the most. Colors & Shapes … In July I was for a few days with friends on a finca in the south of Mallorca and of course I used every opportunity to stick my lens everywhere where there were interesting details to be expected. I don’t know if it had anything to do with the heat, but most of the motifs have a very earthy touch. I probably wanted to stay somewhere underground all the time, because 40 degrees Celsius in the shade was then also a bit too much for me. Usually the best photos end up unnoticed as pool photos here on my blog until I use one as a header image, but now I make a separate post out of each set before I publish the individual photos on other sites like 500px or Pixelfed. Lots of Things Mallorquin Letterbox Basket Parking Aigua Tub Tomato Beauties Veltins Crown Rusty Grate Sticker Direction Table Greenery","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"Mastodon Share Bottom Sheet Dialog","subtitle":"Implement a 'Share Post' dialog in Hexo","date":"2023-09-28","updated":"2023-09-28","path":"post/Mastodon-Share-Bottom-Sheet-Dialog/","permalink":"https://kiko.io/post/Mastodon-Share-Bottom-Sheet-Dialog/","excerpt":"Social media thrives on sharing. Thoughts, experiences, self-dramatizations or even texts or posts from other people. This is especially true for microblogging on (formerly) Twitter and (today) Mastodon. To make this easy, the platforms often offer a SHARE endpoint (URL), such as the famous http://www.twitter.com/share?text=My impossible thoughts on X … or any dirt buttons to collect user data. A few months after the groundbreaking of Mastodon 2016, such a feature was also discussed and implemented on GitHub and out came: https://<your-instance>/share&text=My benevolent thoughts on the Fediverse But … where Twitter had it easy due to its central structure (twitter.com … period.), we Mastodon users all fidget around on different instances, i.e. each instance has its own /SHARE endpoint and so it’s a bit harder to stick a share button on your own blog, because you have to ask the user where he lives. Of course, online services like toot.kytta.dev, s2f.kytta.dev, mastodonshare.com sprouted immediately, but also the button providers expanded their portfolio or new ones were launched, like shareon.js.org, share-on-mastodon.social, shareaholic.com. But seriously … does it take an external service to ask the user for an instance name and redirect him to an URL (and run the risk of falling victim to data collection mania)? Because that’s all it is. All of the above do it exactly that way. So we come to the ready-made developer solutions e.g. How-To tutorials. Here, too, there are a lot of hits after a search: Mastodon-share-button (WebComponent), Mastodon-share-button (JS), Adding a Share On Mastodon button to a website, Adding a “share to mastodon” link to any web site – and here, … and I’ll join them here for my Hexo-driven blog.","keywords":"social media thrives sharing thoughts experiences self-dramatizations texts posts people true microblogging twitter today mastodon make easy platforms offer share endpoint url famous http://www.twitter.com/share?text=My impossible … dirt buttons collect user data months groundbreaking feature discussed implemented github https://<your-instance>/share&text=My benevolent fediverse due central structure twittercom period users fidget instances instance /share bit harder stick button blog lives online services tootkyttadev s2fkyttadev mastodonsharecom sprouted immediately providers expanded portfolio launched shareonjsorg share-on-mastodonsocial shareaholiccom external service redirect run risk falling victim collection mania ready-made developer solutions how-to tutorials lot hits search mastodon-share-button webcomponent js adding website link web site – ill join hexo-driven","text":"Social media thrives on sharing. Thoughts, experiences, self-dramatizations or even texts or posts from other people. This is especially true for microblogging on (formerly) Twitter and (today) Mastodon. To make this easy, the platforms often offer a SHARE endpoint (URL), such as the famous http://www.twitter.com/share?text=My impossible thoughts on X … or any dirt buttons to collect user data. A few months after the groundbreaking of Mastodon 2016, such a feature was also discussed and implemented on GitHub and out came: https://<your-instance>/share&text=My benevolent thoughts on the Fediverse But … where Twitter had it easy due to its central structure (twitter.com … period.), we Mastodon users all fidget around on different instances, i.e. each instance has its own /SHARE endpoint and so it’s a bit harder to stick a share button on your own blog, because you have to ask the user where he lives. Of course, online services like toot.kytta.dev, s2f.kytta.dev, mastodonshare.com sprouted immediately, but also the button providers expanded their portfolio or new ones were launched, like shareon.js.org, share-on-mastodon.social, shareaholic.com. But seriously … does it take an external service to ask the user for an instance name and redirect him to an URL (and run the risk of falling victim to data collection mania)? Because that’s all it is. All of the above do it exactly that way. So we come to the ready-made developer solutions e.g. How-To tutorials. Here, too, there are a lot of hits after a search: Mastodon-share-button (WebComponent), Mastodon-share-button (JS), Adding a Share On Mastodon button to a website, Adding a “share to mastodon” link to any web site – and here, … and I’ll join them here for my Hexo-driven blog. My Mastodon Share VariantIt’s really not hard to develop an appealing looking and working JavaScript solution to the “problem”. Where Christian Heilmann (last link in the list above) relies on a simple window.prompt, I recently introduced my Bottom Sheet dialogs in this blog (here and here), which are perfectly suited for this. Each shareable post should get a SHARE button that lets a bottom sheet pop out where the user can enter their instance name, which is then remembered for next time, and already contains suggested text (title, description, and url of the post) for the Mastodon toot. The ButtonIn every post the interaction section is rendered and there is now a new button, which calls a new dialog method, described later on: themes\\landscape\\layout\\_partial\\post\\interaction.ejs123...<button onclick="dialog.shareOnMastodon();" class="share mastodon">Share on Mastodon</button>... Adding a dialog template to the DOMFor my dialog I first needed a UI, preferably a template in the DOM to avoid having to fiddle with HTML in the JS code: themes\\landscape\\layout\\_partial\\templates\\mastodon-share-dialog.ejs12345678910111213141516171819<template id="mastodon-share-dialog"> <div class="mastodon-share-content"> <section> <p id="mastodon-share-intro"> <img src="/images/mastodon.svg" alt="Mastodon"> There are many Mastodon instances out there. Tell me yours and I will redirect you to the share dialog of your server: </p> <label for="mastodon-instance">Your Mastodon Instance</label> <div id="mastodon-instance-wrapper"> <span>https://</span> <input id="mastodon-instance" name="intance" required /> </div> <label for="mastodon-text">Text to share ...</label> <textarea id="mastodon-text" name="text" rows="8"></textarea> <button id="mastodon-share">Share</button> </section> </div></template> The template is embedded in layout.ejs near the closing HTML tag. It contains mainly an INPUT for the instance name, a TEXTAREA for the suggested text and a button for the action. I’ll leave out the styles here, because if you adopt my implementation, the CSS (or Stylus) will be completely different. Extending the dialog scriptOk … let’s extend the dialog.js code, I first described here, with a new method as used at the button: themes\\landscape\\source\\js\\dialog.js123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960var dpDialog = { 'base': { ... }, 'init': function(options) { ... }, 'show': function(callback) { ... } 'shareOnMastodon': function() { dpDialog.base.init({ headerText: "Share on Mastodon", urlHash: "share", // show hash in url minContentHeight: 400, // limit height of dialog width: "min(600px, 100%)" // center dialog with max width 600px }); } // Grab the template and make it a jQuery object let content = document.getElementById("mastodon-share-dialog").content.cloneNode(true); let jContent = $(content); // Read the cookie if the user has already used the dialog ... let instance = getCookie("mastodon-instance"); if (instance) { // ... and enter it in the INPUT jContent.find("#mastodon-instance").val(instance); } // Create a suggestion text from the page metadata const title = document.querySelector('meta[name="title"]').content; const description = document.querySelector('meta[name="description"]').content; const permalink = document.querySelector('link[rel="canonical"]').href; jContent.find("#mastodon-text").val(title + "\\n\\n" + description + "\\n\\n" + permalink); // Set the BUTTON action ... jContent.find("#mastodon-share").click(function(e) { const eInstance = document.getElementById("mastodon-instance"); const eText = document.getElementById("mastodon-text"); // Let the browser validate the input attribute REQUIRED const isValid = eInstance.reportValidity(); if (isValid) { // Save the entered instance name to a cookie setCookie("mastodon-instance", eInstance.value); // Generate the share URL for the instance let shareUrl = `https://${eInstance.value}/share?text=${encodeURIComponent(eText.value)}`; // Open the share URL in a new window and close the dialog window.open(shareUrl, '_blank'); dpDialog.base.element.downupPopup("close"); } }); // Add the dialog to the DOM jContent.appendTo(dpDialog.base.content); // Open the dialog and set the focus in the INPUT dpDialog.base.show(function() { document.getElementById("mastodon-instance").focus(); });} The ResultThis is it … and here you can see the result of my implementation: … or look at the button in the bottom right corner and use it ;) As I said, it’s just a tool to make it easier for the user to share your content via a URL of their instance to their audience. There is no external stuff, data collection or influence on what is shared. Just JavaScript, HTML running in the browser and one cookie for convenience. You can find all files as usual on the GitHub repo of this blog at https://github.com/kristofzerbe/kiko.io","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Social Media","slug":"Social-Media","permalink":"https://kiko.io/tags/Social-Media/"},{"name":"Mastodon","slug":"Mastodon","permalink":"https://kiko.io/tags/Mastodon/"},{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/tags/JavaScript/"}]},{"title":"Contribute with Conventional Commits","subtitle":"Fun with a Pull Request","date":"2023-09-15","updated":"2023-09-15","path":"post/Contribute-with-Conventional-Commits/","permalink":"https://kiko.io/post/Contribute-with-Conventional-Commits/","excerpt":"I’ll be honest … I need some Git training. From time to time I contribute small things to GitHub projects and sometimes get confused with all the commands. Fork, Clone, Commit, Stage, Pull Request … all things that mean something to me, but that I certainly haven’t internalized. And so it happens that I sometimes mess up a pull request or something similar. Sure, my blog here also lives in GitHub, both in terms of source control and hosting on GitHub Pages, but here I’m the only one committing. No issues, no branches, no pull requests or anything else. I change something, hit commit and I’m done. Another point I can’t dismiss: I’m a Windows guy who likes to click buttons. The command line is not for me at all.What was the name of the parameter? Do I have to write --param=xxx or /param:xxx … damn where is the button?My brain is probably too small for that. Visual Studio Code is a big help there … it has buttons! But that doesn’t save me when it comes to Git, because you have to know in which order to press which of these buttons!","keywords":"ill honest … git training time contribute small things github projects confused commands fork clone commit stage pull request havent internalized mess similar blog lives terms source control hosting pages im committing issues branches requests change hit point dismiss windows guy likes click buttons command line allwhat parameter write --param=xxx /paramxxx damn buttonmy brain visual studio code big doesnt save order press","text":"I’ll be honest … I need some Git training. From time to time I contribute small things to GitHub projects and sometimes get confused with all the commands. Fork, Clone, Commit, Stage, Pull Request … all things that mean something to me, but that I certainly haven’t internalized. And so it happens that I sometimes mess up a pull request or something similar. Sure, my blog here also lives in GitHub, both in terms of source control and hosting on GitHub Pages, but here I’m the only one committing. No issues, no branches, no pull requests or anything else. I change something, hit commit and I’m done. Another point I can’t dismiss: I’m a Windows guy who likes to click buttons. The command line is not for me at all.What was the name of the parameter? Do I have to write --param=xxx or /param:xxx … damn where is the button?My brain is probably too small for that. Visual Studio Code is a big help there … it has buttons! But that doesn’t save me when it comes to Git, because you have to know in which order to press which of these buttons! Yesterday I discovered a small bug in my favorite Mastodon client Elk, which could be solved with one line of CSS. So … cloned the thing, looked for the source file, inserted the line … and googled how to submit the change on my repo clone as a pull request to the Elk project, and found this great tutorial on Medium from someone, with the name Supritha: >>> How to Create a Pull Request on GitHub using VS Code Fantastic. The pull request was a breeze. BUT … on the pull request page of Elk on GitHub the first check threw an error regarding Semantic Pull Request, with the message the title was wrong … what the heck? While I was reading the documentation (after the first attempt to change the title to something reasonable and another failure), one of the Elk developers Joaquín was so nice and corrected the title and pointed me to the very doc I was reading: >>> Conventional Commits <<< Okay … I was already happy that nothing went wrong this time, but that was too early, as so often in IT ;) Here to read: fix(ui): Empty lines in posts from Pixelfed are doubled/tripled (#2392) Since I am a structured person, I liked the commit message rules used by the Elk team, because they open up a space to make the commits easier to evaluate automatically afterwards. I think a great help for projects of this scale … currently 202 contributors! Actually I don’t have a Git project where the Convertional Commits would be useful, but for my future Me … READ THIS POST! (And for my employees, in case they read this: I have an idea that we should definitely establish soon … ;)","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Git/GitHub","slug":"Git-GitHub","permalink":"https://kiko.io/tags/Git-GitHub/"}]},{"title":"Discoveries #26 - JavaScript HowTo's","subtitle":null,"series":"Discoveries","date":"2023-09-12","updated":"2023-09-12","path":"post/Discoveries-26-JavaScript-HowTo-s/","permalink":"https://kiko.io/post/Discoveries-26-JavaScript-HowTo-s/","excerpt":"Over time, everyone accumulates links somewhere about procedures that one has not yet internalized. This is also the case with me and JavaScript development. “Damn … what was that about Call | Proxy | Map | <You name it>?!”. Here are 11 of them … Processing images with sharp in Node.jsUse console.log() like a pro!Simple Swipe with Vanilla JavaScriptThe File System Access API: simplifying access to local filesAn introduction to WebAssembly for JavaScript DevelopersGetting Started with the Map and Set Typed CollectionsJavaScript Currying: A Practical ExampleHow to Use the Call, Apply, and Bind Functions in JavaScriptHow JavaScript's Proxy Object WorksJavaScript waitFor PollingHow to measure page loading time with Performance API","keywords":"time accumulates links procedures internalized case javascript development damn … call | proxy map <you it> processing images sharp nodejsuse consolelog prosimple swipe vanilla javascriptthe file system access api simplifying local filesan introduction webassembly developersgetting started set typed collectionsjavascript currying practical examplehow apply bind functions javascripthow javascript's object worksjavascript waitfor pollinghow measure page loading performance","text":"Over time, everyone accumulates links somewhere about procedures that one has not yet internalized. This is also the case with me and JavaScript development. “Damn … what was that about Call | Proxy | Map | <You name it>?!”. Here are 11 of them … Processing images with sharp in Node.jsUse console.log() like a pro!Simple Swipe with Vanilla JavaScriptThe File System Access API: simplifying access to local filesAn introduction to WebAssembly for JavaScript DevelopersGetting Started with the Map and Set Typed CollectionsJavaScript Currying: A Practical ExampleHow to Use the Call, Apply, and Bind Functions in JavaScriptHow JavaScript's Proxy Object WorksJavaScript waitFor PollingHow to measure page loading time with Performance API Processing images with sharp in Node.js by Pascal Akunne https://blog.logrocket.com/processing-images-sharp-node-js/ Whenever images are to be processed in a NodeJS application, one is well advised to use the Sharp library. Pascal introduces the most important functions in his article. Use console.log() like a pro! by Marko Denic https://denic.hashnode.dev/use-consolelog-like-a-pro Using console.log() for JavaScript debugging is the most common practice among developers. But, there is a lot more, as Marko shows us. Simple Swipe with Vanilla JavaScript by Ana Tudor https://css-tricks.com/simple-swipe-with-vanilla-javascript/ This 2018 article by Ana walks you step-by-step through implementing swipe gestures using an image gallery example with the least amount of code she could think of. The File System Access API: simplifying access to local files by Thomas Steiner & Pete LePage https://web.dev/file-system-access/ The File System Access API allows web apps to read or save changes directly to files and folders on the user's device. Thomas and Peter, both working at Googles Chrome team, show us with examples how this works. An introduction to WebAssembly for JavaScript Developers by Pascal Pares https://pascalpares.appspot.ovh/webassembly-for-javascript-developers/ Pascals article from 2021 is based on the specs of WebAssembly from 2019 and is very useful as an introduction for JavaScript Developers, because it includes a lot of sample code. Getting Started with the Map and Set Typed Collections by Robert Laws https://javascript.plainenglish.io/javascript-getting-started-with-the-map-and-set-typed-collections-2ba173b0ce9f JavaScript has not only the classic arrays to offer, but also other objects for collecting data. Robert has a brief description of Set and Map. JavaScript Currying: A Practical Example by Karthick Ragavendran https://javascript.plainenglish.io/javascript-currying-practical-example-512cf1099e81 In mathematics and computer science, currying is the technique of converting a function that takes multiple arguments into a sequence of functions, each of which takes a single argument … a better description than Karthick’s introduction I could not think of either How to Use the Call, Apply, and Bind Functions in JavaScript by Keyur Paralkar https://www.freecodecamp.org/news/understand-call-apply-and-bind-in-javascript-with-examples/ Call, Apply and Bind are not so common in daily JavaScript development, but they are useful tools because they can be used to change the execution context, as Keyur shows us. How JavaScript's Proxy Object Works by Keyur Paralkar https://www.freecodecamp.org/news/javascript-proxy-object/ Again Keyur. In this post he goes into detail about the proxy object, helps you create another object on behalf of the original object, to get more control over the interaction with the original object JavaScript waitFor Polling by David Walsh https://davidwalsh.name/waitfor By now, every JavaScript developer should be familiar with asynchronous programming using async/await and Promises. But there are use cases where polling makes more sense, as David shows. How to measure page loading time with Performance API by Silvestar Bistrović https://www.silvestar.codes/articles/how-to-measure-page-loading-time-with-performance-api/ MDN recommends using the Performance API to gauge the performance of websites and web applications. It’s useful to show how much time did it take to load a page.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Versengold in Concert","subtitle":"Photos from the performance in Speyer as part of the MPS","date":"2023-09-10","updated":"2023-09-10","path":"post/Versengold-in-Concert/","permalink":"https://kiko.io/post/Versengold-in-Concert/","excerpt":"Since my youth with I hardrock/metal fan, but from good music I let myself convince, even if it does not fit into this scheme. This is what happened with the German medieval/folk band Versengold from Bremen, to whose concert in Bochum my better half dragged me one day. And what can I say … the guys are so much fun with their easy-going manner, their good, funny and sometimes profound German lyrics and their shanty-like music, from which the North German sailor tradition can be clearly heard.","keywords":"youth hardrock/metal fan good music convince fit scheme happened german medieval˿olk band versengold bremen concert bochum half dragged day … guys fun easy-going manner funny profound lyrics shanty-like north sailor tradition heard","text":"Since my youth with I hardrock/metal fan, but from good music I let myself convince, even if it does not fit into this scheme. This is what happened with the German medieval/folk band Versengold from Bremen, to whose concert in Bochum my better half dragged me one day. And what can I say … the guys are so much fun with their easy-going manner, their good, funny and sometimes profound German lyrics and their shanty-like music, from which the North German sailor tradition can be clearly heard. In the meantime we were on two more concerts in Frankfurt, then in Marburg (Open-Air) and in Mainz on the “Night of the Ballads”. Most recently we saw them two weeks ago at the MPS (“Mittelalterlich Phantasie Spectaculum”, a medieval festival) in Speyer, where they played open-air with other bands from the “industry”. On the one hand, I think it’s great that the guys, even if they now fill large halls in Germany, have not forgotten their origins, and on the other hand, I now had the opportunity to get very close to the stage with my camera. Here are the results: let macy_ferwmb = new Macy({ container: '#image-masonry-ferwmb', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); Love you guys…!","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Concert","slug":"Concert","permalink":"https://kiko.io/tags/Concert/"}]},{"title":"SVWW vs. Schalke @ 2023-09-02","subtitle":"The fight of the promoted against the relegated","series":"SV Wehen Wiesbaden","date":"2023-09-02","updated":"2023-09-02","path":"post/SVWW-vs-Schalke-2023-09-02/","permalink":"https://kiko.io/post/SVWW-vs-Schalke-2023-09-02/","excerpt":"1:1 On this game, some in my circle of friends have feverishly awaited, especially my neighbor and friend, who for years is an ardent fan of one of the traditional clubs Schalke 04. We got six additional tickets for the game in time and with a crowd of 11,003 fans, this was also urgently necessary. The stadium (12,566 standing and seated) was full to the roof. Only one of three blocks of the guests was empty. Some Schalke fans seem to have expected nothing from the game in Wiesbaden. No wonder after table position 15 after the last match day. Since season ticket holders get into the stadium a little faster and my friends sat a little scattered in different blocks, I lost sight of them at some point, but that was not tragic, because they had fun. To my delight, the booth operators, who I had to criticize last time, actually did a better job today. Two cash registers: one for cash and another for card/smartphone/watch payers. The sale of beer and bratwurst went much more quickly, only I had to stand in line a bit at the fan shop, because I was not yet recognizable as a fan: it had to get a cap and a jersey, of course, finally.","keywords":"game circle friends feverishly awaited neighbor friend years ardent fan traditional clubs schalke additional tickets time crowd fans urgently stadium standing seated full roof blocks guests empty expected wiesbaden table position match day season ticket holders faster sat scattered lost sight point tragic fun delight booth operators criticize job today cash registers card/smartphone/watch payers sale beer bratwurst quickly stand line bit shop recognizable cap jersey finally","text":"1:1 On this game, some in my circle of friends have feverishly awaited, especially my neighbor and friend, who for years is an ardent fan of one of the traditional clubs Schalke 04. We got six additional tickets for the game in time and with a crowd of 11,003 fans, this was also urgently necessary. The stadium (12,566 standing and seated) was full to the roof. Only one of three blocks of the guests was empty. Some Schalke fans seem to have expected nothing from the game in Wiesbaden. No wonder after table position 15 after the last match day. Since season ticket holders get into the stadium a little faster and my friends sat a little scattered in different blocks, I lost sight of them at some point, but that was not tragic, because they had fun. To my delight, the booth operators, who I had to criticize last time, actually did a better job today. Two cash registers: one for cash and another for card/smartphone/watch payers. The sale of beer and bratwurst went much more quickly, only I had to stand in line a bit at the fan shop, because I was not yet recognizable as a fan: it had to get a cap and a jersey, of course, finally. My fellow fans in row 1 were rather quiet today, despite the good weather and the atmosphere around. In my back there was now and then a comment like “Now RUN!” or “Go ahead. The goal is in THIS direction!”, but I could understand them, because our team hardly dared to cross the center line in the first half. I still don’t know the name of the nice lady next to me, but I will. She suffered today like many rather quietly. The GameIn the first half we were, as they say in the jargon “compact in defense”, which means nothing else, that no one dared to run forward with the ball. Except for once, when after about 20 minutes Kianz Froese, our Canadian striker, ran into the box on the left side at full gallop, but unfortunately missed the goal by a hair’s breadth. Damn! Our team let the opponent play and again straddled every ball off their feet. It wasn’t, however, that Schalke dominated the game. We carelessly let them just do it and the fans had their fun when they shot the ball into the clouds again after a lot of roaring and stomping. Funny. At the beginning of the second half, this continued seamlessly, even if you noticed that the coach in the cabin must have yelled at one or the other and these were now somewhat more active … and suddenly the ball was in our net … 0:1! What? How? A goal in the 54. minute for Schalke out of (almost) nothing! I have to watch that again on TV … :| let macy_25bgg5 = new Macy({ container: '#image-masonry-25bgg5', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); After our guys had digested the shock, after 10 minutes or so, they began … yes indeed … to play football! Also because Kauczinski had replaced a few snoozers. In the last 20 minutes it went with steam on the opponents goal, but we ran slightly out of time. In the 90th minute, 6 minutes of injury time were displayed and it was almost hectic … kick and rush. With success! In the 95th minute it was 1:1, thanks to the leg of Reinthaler, from which bounced a defensive attempt, and in the 97th almost 2:1 … huuuuh, damn close. Before the game I had bet on a 1:1 and was unfortunately/fortunately right. It felt like a victory. ⇾ Match report on kicker.de ConclusionToday we are in 6th place in the table and I don’t really understand why the team was so anxious. Yes, it’s Schalke 04, but they’re further down so far and we certainly don’t need to hide!","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"},{"name":"2. Bundesliga","slug":"2-Bundesliga","permalink":"https://kiko.io/tags/2-Bundesliga/"}]},{"title":"Image Masonry Tag Plugin for Hexo","subtitle":"Easy use of the wonderful Macy.js library to display images in posts","date":"2023-09-01","updated":"2023-09-01","path":"post/Image-Masonry-Tag-Plugin-for-Hexo/","permalink":"https://kiko.io/post/Image-Masonry-Tag-Plugin-for-Hexo/","excerpt":"Displaying a few more images than usual in a post is always a bit tricky, because you have to make sure they don’t get too big and drown out the text. But they should not be too small either and the arrangement is also important to consider. For this purpose I have so far used my Image Slider Tag Plugin, but with this you only ever see one of the images and have to scroll through the rest horizontally. A medium sized overview, best in the so called masonry format, where images are automatically assembled based on their size on a limited area, would be much better for some cases. There are a variety of CSS or JavaScript solutions out there on the net, but the most suitable for me was Macy.js … and how I integrated it into my Hexo environment is what I want to describe here.","keywords":"displaying images usual post bit tricky make dont big drown text small arrangement important purpose image slider tag plugin scroll rest horizontally medium sized overview called masonry format automatically assembled based size limited area cases variety css javascript solutions net suitable macyjs … integrated hexo environment describe","text":"Displaying a few more images than usual in a post is always a bit tricky, because you have to make sure they don’t get too big and drown out the text. But they should not be too small either and the arrangement is also important to consider. For this purpose I have so far used my Image Slider Tag Plugin, but with this you only ever see one of the images and have to scroll through the rest horizontally. A medium sized overview, best in the so called masonry format, where images are automatically assembled based on their size on a limited area, would be much better for some cases. There are a variety of CSS or JavaScript solutions out there on the net, but the most suitable for me was Macy.js … and how I integrated it into my Hexo environment is what I want to describe here. Like (Tiny Slider), Macy.js is also based on JavaScript, as the name already expresses. The setup in HTML is very simple: a certain number of wrappers are arranged in a container, each of which contains an image: 1234567891011121314151617181920212223<div id="macy-container"> <div> <img src="/photos/normal/D50_0053.jpg" alt=""> </div> <div> <img src="/photos/normal/D50_0075.jpg" alt=""> </div> <div> <img src="/photos/normal/D50_0086.jpg" alt=""> </div> <div> <img src="/photos/normal/D50_1092.jpg" alt=""> </div> <div> <img src="/photos/normal/D50_1147.jpg" alt=""> </div> <div> <img src="/photos/normal/D50_1577.jpg" alt=""> </div> <div> <img src="/photos/normal/_D50_3251.jpg" alt=""> </div></div> It does not matter whether the images are the same size or whether they are in portrait or landscape format. Macy.js then takes care of the sensible arrangement of the images in the container. All that is missing now is the call to the script: 1234567891011121314151617181920212223let macy = new Macy({ container: '#macy-container', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 1024: { margin: { x: 8, y: 8 }, columns: 4 }, 768: 3 }}); For more information on the available parameters (and there are some interesting), please visit https://github.com/bigbite/macy.js. The Tag PluginTo make it easy for Hexo users, I created a tag plugin from the code above and added it to my Hexo Tag Plugin Collection on GitHub. Usage Example: 12345678910{% image_masonry "../../photos/normal/D50_0053.jpg|Thomas' Ruby Prince I" "../../photos/normal/_D50_3251.jpg|No Name" "../../photos/normal/D50_0086.jpg|Thomas' German Flag" "../../photos/normal/D50_1147.jpg|Poppy Green" "../../photos/normal/D50_0075.jpg|Thomas Wild Tulips" "../../photos/normal/D50_7474.jpg|Garden Beauties XIV" "../../photos/normal/D50_4451.jpg|Garden Beauties I" "../../photos/normal/D50_1577.jpg|Floral Magic XIV"%} Live Output: let macy_i7ci44 = new Macy({ container: '#image-masonry-i7ci44', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } });","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"}]},{"title":"Convert HTML into Plain Text in Hexo","subtitle":"Render Page.Excerpt into META Tag","series":"A New Blog","date":"2023-08-31","updated":"2023-08-31","path":"post/Convert-HTML-into-Plain-Text-in-Hexo/","permalink":"https://kiko.io/post/Convert-HTML-into-Plain-Text-in-Hexo/","excerpt":"Hexo, on which this blog is based, is a Static Site Generator (SSG) that generates a whole structure of HTML files from individual Markdown files in which the articles were written. Besides the actual posts, also overview pages like the archives and others. For the latter, however, it only needs an excerpt from the actual article, which Hexo automatically creates from the initially generated HTML content and which is also available as HTML. For my Page Meta dialog, however, I recently needed the excerpt as plain text to make it easier to transfer it manually to a Mastodon post, for example. My initial attempts to extract the plain text from the original Markdown turned out to be quite difficult, because in Hexo not only Markdown is used, but also special Tag Plugins in Nunjucks format and of course plain HTML. Long speech, short sense … after the first dozen RegEx-Replace calls, I got doubts to be on the right way and remembered Page.Excerpt, the variant already generated by Hexo in HTML.","keywords":"hexo blog based static site generator ssg generates structure html files individual markdown articles written actual posts overview pages archives excerpt article automatically creates initially generated content page meta dialog recently needed plain text make easier transfer manually mastodon post initial attempts extract original turned difficult special tag plugins nunjucks format long speech short sense … dozen regex-replace calls doubts remembered pageexcerpt variant","text":"Hexo, on which this blog is based, is a Static Site Generator (SSG) that generates a whole structure of HTML files from individual Markdown files in which the articles were written. Besides the actual posts, also overview pages like the archives and others. For the latter, however, it only needs an excerpt from the actual article, which Hexo automatically creates from the initially generated HTML content and which is also available as HTML. For my Page Meta dialog, however, I recently needed the excerpt as plain text to make it easier to transfer it manually to a Mastodon post, for example. My initial attempts to extract the plain text from the original Markdown turned out to be quite difficult, because in Hexo not only Markdown is used, but also special Tag Plugins in Nunjucks format and of course plain HTML. Long speech, short sense … after the first dozen RegEx-Replace calls, I got doubts to be on the right way and remembered Page.Excerpt, the variant already generated by Hexo in HTML. Now you would think that JavaScript has some built-in function to extract the plain text out of a bunch of HTML tags, but this is actually not the case. You have to take a little detour to do this: 1234567function convertHtml2PlainText(excerpt) { let e = document.createElement("div"); e.innerHTML = excerpt; return e.textContent || e.innerText;}let plainText = convertHtml2PlainText(page.excerpt); Fine, my problem is solved … hmm… NO, because Node.js does not know a document, because a DOM exists only in the browser. But … there are libraries like jsdom that make a DOM available in Node.js: 12345678910const { JSDOM } = require("jsdom");function convertHtml2PlainText(excerpt) { const dom = new JSDOM('<!DOCTYPE html>'); let e = dom.window.document.createElement("div"); e.innerHTML = excerpt; return e.textContent || e.innerText;}let plainText = convertHtml2PlainText(page.excerpt); Nice … but also doesn’t work, because I need the piece of code in an EJS template, but when processing the same to HTML, the included JavaScript code is executed, but loading external libraries via require() is not supported. And once again Hexo’s Tag Helpers come to my rescue: helper-excerpt-plain.js12345678910const { JSDOM } = require("jsdom");hexo.extend.helper.register('excerpt_plain', function(excerpt){ const dom = new JSDOM('<!DOCTYPE html>'); let e = dom.window.document.createElement("div"); e.innerHTML = excerpt; return e.textContent || e.innerText;}); For the sake of beauty, I also cut out leading and double line breaks after the conversion and put the result in a custom meta tag in the head of the HTML page to have access to it later via JavaScript running in the browser: head.ejs123456789101112...let excerpt = excerpt_plain(page.excerpt) .replace(/^(\\r\\n|\\n|\\r)/, "") // Remove leading break .replace(/(\\r\\n|\\n|\\r){2,}/g, " ") // Remove multiple breaks .trim();...<meta name="excerpt" content="<%= excerpt %>">... Et voilá … I have my excerpt as plain text to show in my Page Meta dialog.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Node.js","slug":"Node-js","permalink":"https://kiko.io/tags/Node-js/"},{"name":"Meta","slug":"Meta","permalink":"https://kiko.io/tags/Meta/"}]},{"title":"Pool Photo Generator","subtitle":"How to create multiple device dependent header photos with Node","date":"2023-08-20","updated":"2023-08-20","path":"post/Pool-Photo-Generator/","permalink":"https://kiko.io/post/Pool-Photo-Generator/","excerpt":"Since the existence of this blog, the posts all have a custom header image that I generate from my own photos. Already three years ago (omg … really?) I described in an article how to do this with Hexo: Automatic Header Images in Hexo. To keep it short, I use a pool folder for this, in which I keep in subfolders next to a meta.txt, for the title of the image and an external url on 500px for interactions to the image, three variants that I need for a new post: mobile.jpg (width 480px) tablet.jpg (width 768px) normal.jpg (width 1280px) The only piece of the puzzle that was still missing was the automatic generation of these three image variants and the meta file based on a selected photo that I want to add to the pool of available header images. So far it was fun to generate the header images manually either on the desktop or on the smartphone, but it really doesn’t have to be. My goal now was to write a script where I just throw a selected photo into a folder and the NodeJS script does the rest. My photo workflow is based on Adobe Lightroom Classic and one of the steps is to give a title to the good ones I use here as well. So the script had to include four steps when iterating over the inbound folder’s JPG files: create new pool folder read meta data (IPTC -> title) and write it to meta.txt create the three image variants delete the processed image from the inbound folder","keywords":"existence blog posts custom header image generate photos years ago omg … article hexo automatic images short pool folder subfolders metatxt title external url 500px interactions variants post mobilejpg width 480px tabletjpg 768px normaljpg 1280px piece puzzle missing generation meta file based selected photo add fun manually desktop smartphone doesnt goal write script throw nodejs rest workflow adobe lightroom classic steps give good include iterating inbound folders jpg files create read data iptc -> delete processed","text":"Since the existence of this blog, the posts all have a custom header image that I generate from my own photos. Already three years ago (omg … really?) I described in an article how to do this with Hexo: Automatic Header Images in Hexo. To keep it short, I use a pool folder for this, in which I keep in subfolders next to a meta.txt, for the title of the image and an external url on 500px for interactions to the image, three variants that I need for a new post: mobile.jpg (width 480px) tablet.jpg (width 768px) normal.jpg (width 1280px) The only piece of the puzzle that was still missing was the automatic generation of these three image variants and the meta file based on a selected photo that I want to add to the pool of available header images. So far it was fun to generate the header images manually either on the desktop or on the smartphone, but it really doesn’t have to be. My goal now was to write a script where I just throw a selected photo into a folder and the NodeJS script does the rest. My photo workflow is based on Adobe Lightroom Classic and one of the steps is to give a title to the good ones I use here as well. So the script had to include four steps when iterating over the inbound folder’s JPG files: create new pool folder read meta data (IPTC -> title) and write it to meta.txt create the three image variants delete the processed image from the inbound folder The ScriptI implemented the script as a class with the following skeleton: pool-photo-generator.cjs123456789101112131415161718192021222324252627282930'use strict';[requirements ...][vars ...]class PoolPhotoGenerator { /** * Contructor of PoolPhotoGenerator * @param {String} inboundFolder * @param {String} poolFolder */ constructor(inboundFolder, poolFolder) { ... } /** * Runs the generation of inbound photos to pool photos */ generate() { ... } /** * Helper function to create image variant * @param {String} imgSource * @param {String} imgTarget * @param {Number} sizeWidth */ async createImageVariant(imgSource, imgTarget, sizeWidth) { ... }}module.exports.PoolPhotoGenerator = PoolPhotoGenerator; RequirementsTo handle files and folders in NodeJS you need at least fs and path: 12const fs = require("fs");const path = require("path"); For image processing there’s no better solution as Sharp: 12const sharp = require('sharp');sharp.cache(false); //prevents keeping source file open Similarly powerful, but intended for reading image metadata is EXIFR: 1const exifr = require('exifr'); VarsI just needed three vars for holding the full qualified path of the current execution folder and the names of the two incoming parameters: 1234const _currentPath = __dirname;let _inboundFolder;let _poolFolder; ConstructorIn this case, the constructor only serves to provide and check the necessary parameters of the class: 123456789101112constructor(inboundFolder, poolFolder) { _inboundFolder = path.join(_currentPath, inboundFolder); _poolFolder = path.join(_currentPath, poolFolder); if (!fs.existsSync(_inboundFolder)) { throw "Inbound folder not found" } if (!fs.existsSync(_poolFolder)) { throw "Pool folder not found" }} Function ‘generate’This is the main function to call, and it first reads the input folder for JPG and cycles through all the hits. Then for each file the above four steps are executed: 12345678910111213141516171819202122232425262728293031323334353637383940generate() { let self = this; const inboundFiles = fs.readdirSync(_inboundFolder); const jpgFiles = inboundFiles.filter(file => { return path.extname(file).toLowerCase() === ".jpg"; }); jpgFiles.forEach((file) => { const imgFile = path.join(_inboundFolder, file); // Step 1: Create new pool folder const newPhotoFolder = path.join(_poolFolder, file.replace(path.extname(file), '')); fs.mkdirSync(newPhotoFolder); // Step 2: Read TITLE from IPTC and write to meta.txt const iptcMeta = exifr.parse(imgFile, { iptc: true }).then(output => { let title = output.ObjectName || "No Title"; fs.writeFile(path.join(newPhotoFolder, "meta.txt"), title); }); // Step 3: Create image variants const createMobile = self.createImageVariant(imgFile, path.join(newPhotoFolder, "mobile.jpg"), 480); const createTablet = self.createImageVariant(imgFile, path.join(newPhotoFolder, "tablet.jpg"), 768); const createNormal = self.createImageVariant(imgFile, path.join(newPhotoFolder, "normal.jpg"), 1280); // Step 4: Delete processed JPG in inbound folder, when everything is done Promise.all([ iptcMeta, createMobile, createTablet, createNormal ]).then(() => { fs.unlinkSync(imgFile); }); }} Function ‘createImageVariant’This helper function reduces the original image to the desired size and saves it in the destination (pool) folder as a JPG: 12345678910async createImageVariant(imgSource, imgTarget, sizeWidth) { await sharp(imgSource) .resize({ fit: sharp.fit.contain, width: sizeWidth }) .jpeg({ quality: 90, mozjpeg: true }) .toFile(imgTarget);} In the above code I have omitted some syntactical sugar. You can find the complete script here: https://github.com/kristofzerbe/kiko.io/blob/master/lib/pool-photo-generator.cjs The RunnerI integrated the call to the generator into my Hexo workflow, but also wrote a small runner to run it independently: 1234567891011121314/** * This is only for executing the selector manually. * * Execution: * node "./lib/_run_pool-photo-generator.cjs" */const PoolPhotoGenerator = require("../lib/pool-photo-generator.cjs").PoolPhotoGenerator;const inboundFolder = "../new_photos"; //my inbound folderconst poolFolder = "../static/pool"; //my pool folderconst generator = new PoolPhotoGenerator(inboundFolder, poolFolder);generator.generate(); ConclusionLast but not least, in my existing Lightroom workflow, I configured the wonderful plugin Jeffrey’s “Collection Publisher” to create the new pool photos directly through it into the Inbound folder. Once the changes are committed to Github, where the blog is hosted and the deployment action happens, the new header images are created and displayed at https:\\kiko.io\\photos and are available for a new post.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"},{"name":"Node.js","slug":"Node-js","permalink":"https://kiko.io/tags/Node-js/"}]},{"title":"SVWW vs. Karlsruhe @ 2023-08-18","subtitle":"My first home game in this season","series":"SV Wehen Wiesbaden","date":"2023-08-19","updated":"2023-08-19","path":"post/SVWW-vs-Karlsruhe-2023-08-18/","permalink":"https://kiko.io/post/SVWW-vs-Karlsruhe-2023-08-18/","excerpt":"1:0 I already had my season ticket on the first matchday of the 2023/2024 season, but was unable to get into the stadium for the team’s first home game and thus missed the 1:1 draw against Magdeburg. First ImpressionsSo, Friday was my premiere on my season seat West 3, Row 1, Seat 1. That this seat was free when I booked it was a coincidence, but being so close to the pitch appealed to me. In front of me, the concrete railing of the spectator block, where I can put my beer, and directly below it, the SVWW coach’s bench under a plexiglass roof. You can hardly get any closer to the team.","keywords":"season ticket matchday 2023 unable stadium teams home game missed draw magdeburg impressionsso friday premiere seat west row free booked coincidence close pitch appealed front concrete railing spectator block put beer directly svww coachs bench plexiglass roof closer team","text":"1:0 I already had my season ticket on the first matchday of the 2023/2024 season, but was unable to get into the stadium for the team’s first home game and thus missed the 1:1 draw against Magdeburg. First ImpressionsSo, Friday was my premiere on my season seat West 3, Row 1, Seat 1. That this seat was free when I booked it was a coincidence, but being so close to the pitch appealed to me. In front of me, the concrete railing of the spectator block, where I can put my beer, and directly below it, the SVWW coach’s bench under a plexiglass roof. You can hardly get any closer to the team. However, the division of the row of seats is quite stupid, with the separation of Block West 2 and West 3 being quite arbitrary. With the result that I have to push my way to seat 1 across almost 20 seats if I am late. From the entrance of block West 2, on the other hand, it would only be just under 10 seats, but the stewards won’t let me get to my seat via West 2 because of my card and for the sake of German order :| My seat neighbour on the right side is a man of the “Do I have to talk?” type who can hardly get his teeth apart even during the game. On my left, however, sits a nice older lady with short grey hair who must already be part of the inventory. A permanent season ticket holder, I guess, who doesn’t yell all the time either, but seems to be a true fan. The first question directed at me was “But you weren’t there for the first game!?”, which immediately triggered a kind of guilt in me, “Yes, I didn’t cheer for them in the first game. I’m sooo sorry”. We’ll get on well, especially as I took her deposit cup at half-time and cashed it in. We’re already friends, even if I don’t know her name yet. Speaking of beverage supply: The club has a lot of stands around the stadium, but they are so badly organised that it’s hard to make it back to your seat in time. There were four staff in the snack van right next to Block West 3, one for ordering and collecting money and the other three for serving. The cashier was so slow that the other three had nothing to do the whole time and the queue in front of the stand got longer and longer and some people just gave up. Dear operator: This would happen much faster with two cashiers. It may be that they are all volunteers, but that can be organised much better for the benefit of the fans! In the row directly behind me sits a group of older men who spout rather platitudinous football wisdom all the time. These guys would be a perfect model for the German (but unfortunately already deceased) comedian Loriot :DSitting next to them, however, are two or three guys in their 50s who don’t really have their emotions under control. One of them, in particular, yells “Referee, hang yourself” or something similarly stupid and insults opposing players when they go down in a tackle. I, on the other hand, only ever shouted positively at my own boys. “Let’s go”, “Go on, go on, go on”. And of course I sing along, because no one here notices how “well” I can do that ;) The GameThe club has of course strengthened itself for the new season with new players in all sections of the team, whom I have now seen play for the first time. And unfortunately, some of them, such as Brooklyn Ezeh and Benedikt Hollerbach, who grew on me during the few games last year, have moved to other clubs. let macy_iof6tw = new Macy({ container: '#image-masonry-iof6tw', trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 980: { margin: { x: 8, y: 8 }, columns: 3 }, 768: 2, 640: 3 } }); But we quickly saw that our coach, Markus Kauczinski, knows his ex-club Karlsruher FC well. At the beginning of the first half, the players of this traditional club directly tried to overrun our team, seen by 10,626 spectators. There was a lot of pressure on the defence, but it held up magnificently. There was always a Wiesbaden leg between the ball and the goal and we limited ourselves to counterattacks, which led to success in the 22nd minute when the newcomer from Bayern Munich Lee Hyun-Ju marked the 1:0. The rest of the first half was quite even, because Karlsruhe had lost the momentum a little bit, but they found it again at the beginning of the second half. Again, the game ran in only one direction, our goal, but the last 16 metres, in the box, were impregnable for them and when they did manage it, they failed because of our goalkeeper Florian Stritzel. ⇾ Match report on kicker.de ConclusionSV Wehen Wiesbaden is certainly the underdog in the 2. Bundesliga and no fan expects that at the end of the season it will be about more than keeping the league. I like underdogs and am therefore completely thrilled about this victory, especially since the table looked really good at the end of the day: In two weeks, an even bigger German football heavyweight will come to Wiesbaden: Schalke 04. Let’s see if the boys have grown a few more legs by then. We will need them …","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"},{"name":"2. Bundesliga","slug":"2-Bundesliga","permalink":"https://kiko.io/tags/2-Bundesliga/"}]},{"title":"My Hometown, My Club","subtitle":"How I became a football fan over 50","series":"SV Wehen Wiesbaden","date":"2023-08-19","updated":"2023-08-19","path":"post/My-Hometown-My-Club/","permalink":"https://kiko.io/post/My-Hometown-My-Club/","excerpt":"I was born in Wiesbaden (Hesse, Germany) and I consider this city my home, even though my father built a house in a small suburb called Taunusstein-Wehen in the 70s and I practically grew up there. In this small town, where practically everybody knows everybody, there is a small football club called SV Wehen since 1926 and some of my schoolmates played there in their youth. This small club played only a regional role at all until 1979, when a local business man put money into the club and it worked its way up one league after the other over the years:","keywords":"born wiesbaden hesse germany city home father built house small suburb called taunusstein-wehen 70s practically grew town football club sv wehen schoolmates played youth regional role local business man put money worked league years","text":"I was born in Wiesbaden (Hesse, Germany) and I consider this city my home, even though my father built a house in a small suburb called Taunusstein-Wehen in the 70s and I practically grew up there. In this small town, where practically everybody knows everybody, there is a small football club called SV Wehen since 1926 and some of my schoolmates played there in their youth. This small club played only a regional role at all until 1979, when a local business man put money into the club and it worked its way up one league after the other over the years: 1983 Bezirksliga (amateurs)⇣1987 Landesliga (amateurs)⇣1989 Oberliga (amateurs)⇣1994 Regionalliga (semi-professional)⇣2007 2. Bundesliga (professional) However, with the club’s promotion to the second highest and now professional league of German football, the requirements for the stadium and the necessary infrastructure also changed and the old one in Wehen did not meet the requirements of the officials. So the professional football department was spun off into a company called SV Wehen Wiesbaden based in the nearby Hessian capital, where the new Brita Arena was built. They were relegated to the newly (in 2008) formed 3. Bundesliga after 2 years, but the new club remained in Wiesbaden and in 2020 it was again promoted to the 2. Bundesliga, but was relegated again one year later. I was always only semi-interested in football, especially since I became a fan of the Hamburger Sportverein (HSV) by chance in the early 80s, but in 2022 I got free tickets to a SV Wehen Wiesbaden game through my wife’s colleague … and it was actually really fun, with a cup of beer and a bratwurst, to cheer on the guys on the pitch who were playing professional football in my hometown. I was a little bit hooked … At the end of the 2022/2023 season, the club finished 3rd in the 3. Bundesliga, having missed out on direct promotion, but qualifying for relegation against the third last-place team in the next higher league: Arminia Bielefeld. The two incredible relegation matches were actually won by the underrated club, 4:0 and 2:1 respectively. YESSSS! Somehow it seemed clear to me to go to the stadium more often from now on and the fact that this year the 2nd league is peppered with German traditional clubs (like HSV, my childhood club) made me buy a season ticket for the first time: Block West 3, Row 1, Seat 1 … \\o/.","categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"}]},{"title":"CONTINUE READING Link & Auto Scrolling on the called page","subtitle":"Help your user to read on directly","date":"2023-07-29","updated":"2023-07-29","path":"post/CONTINUE-READING-Link-Auto-Scrolling-on-the-called-page/","permalink":"https://kiko.io/post/CONTINUE-READING-Link-Auto-Scrolling-on-the-called-page/","excerpt":"On the home page of a blog or other text-heavy site with regular new articles, it is often advisable not to display the entire text of the article, but a more or less large excerpt and a READ MORE or CONTINUE READING link that leads to the rest of the article, usually a stand-alone article page. This allows the user to quickly get a picture of, say, the last dozen posts when he comes here to browse your texts. However, it is somewhat unpleasant if you as the author decide to display a larger excerpt after all, and the user lands at the top of the called page after clicking on the MORE link and first has to scroll/navigate to the right place until he can resume reading. This destroys his reading flow. It is better to take the user directly to the page where the MORE link interrupted the text on the home page. With a hash and some JavaScript this is done so quickly, that I wonder why I haven’t implemented this on my own blog already :)","keywords":"home page blog text-heavy site regular articles advisable display entire text article large excerpt read continue reading link leads rest stand-alone user quickly picture dozen posts browse texts unpleasant author decide larger lands top called clicking scroll/navigate place resume destroys flow directly interrupted hash javascript havent implemented","text":"On the home page of a blog or other text-heavy site with regular new articles, it is often advisable not to display the entire text of the article, but a more or less large excerpt and a READ MORE or CONTINUE READING link that leads to the rest of the article, usually a stand-alone article page. This allows the user to quickly get a picture of, say, the last dozen posts when he comes here to browse your texts. However, it is somewhat unpleasant if you as the author decide to display a larger excerpt after all, and the user lands at the top of the called page after clicking on the MORE link and first has to scroll/navigate to the right place until he can resume reading. This destroys his reading flow. It is better to take the user directly to the page where the MORE link interrupted the text on the home page. With a hash and some JavaScript this is done so quickly, that I wonder why I haven’t implemented this on my own blog already :) The principle is quite simple: append a hash to the MORE link URL the user will click detect the hash on the called article page and scroll via JavaScript to an anchor element that was rendered here instead of the MORE link remove the hash from the URL again Some pre-explanation of my exampleMy blog is based on the static website generator Hexo. So I write my articles in Markdown and use for the interruption of an article a built-in helper, which uses the comment <!-- more --> to hack the Content into two parts (Excerpt and More) and replaces it with simple <span id="more"></span>. I use the Excerpt for the start and other overview pages and the Content for the article pages themselves. Expand the MORE linkIn my template for the start page, 8 articles are currently rendered into the page via a separate EXCERPT template. This contains the code for the MORE link (excerpt_link in my example): ../layout/partial/excerpt.ejs12345678910111213141516...<div class="article-entry"> <%- post.excerpt %> <% if (theme.excerpt_link){ %> <p class="article-more-link"> <a href="<%- url_for(post.path) + '#continue' %>"> <%= theme.excerpt_link %> </a> </p> <% } %></div>... I simply appended the hash “#continue“ to the url_for(post.path) statement and this is it for the start page. The JS on the article pageSince some of my pages deal with hashes, I have a general script that loads in the footer of each page and checks the hashes passed to the page. In the switch statement I only needed a new case for continue. The first thing I did was to remove the hash again from the because I don’t want to show that to the user. It is only a tool for a better reading flow and should not have any further effects. To scroll around on a page via JavaScript, there is the scrollIntoView method, which provides a wonderfully smooth scrolling effect with the behavior: "smooth" and inline: "nearest" options. In my case, however, this was out of the question, since it does not support an offset, which I need for my fixed header. The solution was the classic window.scrollTo() function, which is a bit more work, but you can specify exactly where you want the page to go. ../layout/partial/after-footer.ejs123456789101112131415161718192021<script>var hash = window.location.hash.substr(1);switch (hash.toLowerCase()) { ... case "continue": history.replaceState(null, null, ' '); // remove hash window.scrollTo({ // scroll to the MORE element in the Article behavior: 'smooth', top: document.querySelector("#more").getBoundingClientRect().top - document.body.getBoundingClientRect().top - 200, // with a little buffer around it }); break; default: break;}</script> This is it …","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"UI","slug":"UI","permalink":"https://kiko.io/tags/UI/"},{"name":"Usability","slug":"Usability","permalink":"https://kiko.io/tags/Usability/"}]},{"title":"Extension of downupPopup: Back Button, Escape Key & More","subtitle":"Contributing to Ali Dinçer's Bottom Sheet project","date":"2023-07-04","updated":"2023-07-04","path":"post/Extension-of-downupPopup-Back-Button-Escape-Key-More/","permalink":"https://kiko.io/post/Extension-of-downupPopup-Back-Button-Escape-Key-More/","excerpt":"I recently introduced a Bottom Sheet dialog on this blog to display a page’s metadata (like this), using Ali Dincer’s work: downupPopup. I described the way to do this in a post a couple three weeks ago. Shortly after that Koos Looijensteijn triggered me with his post How to make digital business cards and share them via QR codes and I felt like using my newly introduced dialog manager based on downupPopup for my own contact card.But more about that at a later time, respectively blog post… Important for this post is that Ali’s bottom sheet solution did not offer everything I wanted for my implementation: 1. Make Dialog ReusableAs I have already described in the above mentioned article: Ali’s approach to calling the dialog was to create and initialize the necessary HTML elements if it didn’t already exist in the DOM. I pulled out the initialization to make the component reusable. There is now a preparation part and an initialization part and the latter is always called, no matter if another bottom sheet dialog was already created before. My GitHub commit on this part…","keywords":"recently introduced bottom sheet dialog blog display pages metadata ali dincers work downuppopup post couple weeks ago shortly koos looijensteijn triggered make digital business cards share qr codes felt newly manager based contact cardbut time post… important alis solution offer wanted implementation reusableas mentioned article approach calling create initialize html elements didnt exist dom pulled initialization component reusable preparation part called matter created github commit part…","text":"I recently introduced a Bottom Sheet dialog on this blog to display a page’s metadata (like this), using Ali Dincer’s work: downupPopup. I described the way to do this in a post a couple three weeks ago. Shortly after that Koos Looijensteijn triggered me with his post How to make digital business cards and share them via QR codes and I felt like using my newly introduced dialog manager based on downupPopup for my own contact card.But more about that at a later time, respectively blog post… Important for this post is that Ali’s bottom sheet solution did not offer everything I wanted for my implementation: 1. Make Dialog ReusableAs I have already described in the above mentioned article: Ali’s approach to calling the dialog was to create and initialize the necessary HTML elements if it didn’t already exist in the DOM. I pulled out the initialization to make the component reusable. There is now a preparation part and an initialization part and the latter is always called, no matter if another bottom sheet dialog was already created before. My GitHub commit on this part… 2. Dynamic DistanceSince it makes no sense to have to scroll around on a contact card if the space provided by the web designer is not sufficient at small resolutions, I need a more dynamic approach to Ali’s distance, which sets the distance of the bottom sheet to the top of the viewport. To do this, I introduced a new setting called minContentHeight that can specify in pixels how high the content must be at least for everything to be visible. Because this makes the possibly specified distance absurd, I overwrite this specification with a value calculated with it and only then enter the distance value into the element attribute for further use. For the calculation to be correct, however, I need a fixed value for the HEADER of the dialog at this point, which was previously only defined in the CSS. In retrospect, this was a good decision, because once slight pixel shifts between HEADER and CONTENT disappeared in Google’s Chrome, my preferred browser. downupPopup.js12345678910111213141516171819202122232425262728293031// Option Handling...var settings = $.extend({ ... minContentHeight: null, ...});// Initialization...// setting header heightconst $head = $this.find(".downupPopup-header");$head.find("span").text(settings.headerText);const headH = 6;$head.css('height', '' + headH + 'vh');// calculating dynamic distance by given minContentHeightif (settings.minContentHeight) { let calcDistance = Math.round((100 * (window.innerHeight - settings.minContentHeight) / window.innerHeight)) - headH; settings.distance = Math.max(0, calcDistance);}$this.attr('distance', settings.distance);// setting distance to topconst $cont = $this.find(".downupPopup-content");const contH = 100 - settings.distance - headH;$cont.css('height', '' + contH + 'vh');... My GitHub commit on this part… 3. Closing Dialog by ESC Key or BACK ButtonWith Koos’ really nice contact card, I noticed that I automatically tried to close his SHARE DATA popup dialog with the QRCode, which is created on-the-fly with JavaScript, with the BACK button, but ended up on a completely different, previously called page. I guess this is a psychological thing: if an element not bound to the url overlaps the current page, people (or just me?) perceives it as an independent page and try to navigate with long learned methods. So what was missing was, on the one hand, the ability to close the dialog using the ESCAPE key (for desktop users) and, on the other hand, to manipulate the URL history so that the BACK button or the corresponding mobile gesture works as expected. For the latter I needed again a new setting: the hash which will be added to the current URL to make the BACK button work: urlHash. In addition, there were several redundant places in the original code, with which the dialog was closed, which I have summarized in a function, in order to avoid the new necessary to write several times. (my GitHub commit on this part). downupPopup.js1234567891011121314151617181920212223242526272829303132333435363738394041424344// Option Handling...var settings = $.extend({ ... urlHash: null, ...});// General CLOSE functionfunction close() { ... // unbind ESC $(document).off('keyup'); // remove url hash & unbind BACK button if (settings?.urlHash || $this.attr('hash')) { history.replaceState(null, null, ' '); $(window).off("popstate"); }}// Initialization...// bind ESC to close$(document).on('keyup', function(event) { if(event.key == "Escape") { close(); }});if (settings.urlHash) { // set url hash & bind BACK button to close window.location.hash = settings.urlHash; $(window).on("popstate", function(){ close(); });}$this.attr('hash', settings.urlHash);... The key of this code regarding the BACK button is to put the passed hash into the URL when initializing the dialog via window.location.hash and to make sure that it is closed when the hash is removed again by the BACK button. I achieved this by using the popstate event, which fires when the active history entry changes. When the dialog is about to close, I had to make sure that the history entry is overwritten via replaceState with null (delete) and remove the previously bound popstate event again, to avoid side effects. The code for the ESC key is similar: Bind the keyup event on startup to close the dialog and remove it again on close. My GitHub commit on this part… ConclusionEverything works as expected and I hope that Ali will process my pull request soon. Until then you can find the code in my fork. All in all, the script has not become more readable or easier to maintain, even by using jQuery. In my head I already have the idea to do a complete rewrite as ESM class. Reason for this is surely Ali’s approach to realize commands like ‘open’ and ‘close’ as string parameters. This works better than real functions of an instantiated object. Furthermore, it is necessary to cache settings like distance and urlHash in HTML attributes, which does not have to be this way.","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"jQuery","slug":"jQuery","permalink":"https://kiko.io/tags/jQuery/"},{"name":"UI","slug":"UI","permalink":"https://kiko.io/tags/UI/"},{"name":"Contributing","slug":"Contributing","permalink":"https://kiko.io/tags/Contributing/"}]},{"title":"Discoveries #25 - Tutorials & HowTo's","subtitle":null,"series":"Discoveries","date":"2023-07-01","updated":"2023-07-01","path":"post/Discoveries-25-Tutorials-HowTo-s/","permalink":"https://kiko.io/post/Discoveries-25-Tutorials-HowTo-s/","excerpt":"This Discoveries issue is generally about tutorials from different areas: CORS, Azure, Elektron, GitHub Action, SVG. Outstanding articles by outstanding authors, who explain things in an easy and understandable way. Happy Reading… CS Visualized: CORSUse Azure Functions, Azure Storage blobs, and Cosmos DB to copy images from public URLsBuild a Secure Desktop App with Electron Forge and ReactNode.js API Authentication with JSON Web TokensHow To Build an SMTP Mail Server with Express, Node, and GmailAdvanced Git Series 1/8: Creating the Perfect Commit in GitLearn how to use Git and GitHub in a team like a proHow to Build Your First JavaScript GitHub ActionGetting the Gist of GitHub ActionsSwipey Image Grids (with SVG)","keywords":"discoveries issue generally tutorials areas cors azure elektron github action svg outstanding articles authors explain things easy understandable happy reading… cs visualized corsuse functions storage blobs cosmos db copy images public urlsbuild secure desktop app electron forge reactnodejs api authentication json web tokenshow build smtp mail server express node gmailadvanced git series 1/8 creating perfect commit gitlearn team prohow javascript actiongetting gist actionsswipey image grids","text":"This Discoveries issue is generally about tutorials from different areas: CORS, Azure, Elektron, GitHub Action, SVG. Outstanding articles by outstanding authors, who explain things in an easy and understandable way. Happy Reading… CS Visualized: CORSUse Azure Functions, Azure Storage blobs, and Cosmos DB to copy images from public URLsBuild a Secure Desktop App with Electron Forge and ReactNode.js API Authentication with JSON Web TokensHow To Build an SMTP Mail Server with Express, Node, and GmailAdvanced Git Series 1/8: Creating the Perfect Commit in GitLearn how to use Git and GitHub in a team like a proHow to Build Your First JavaScript GitHub ActionGetting the Gist of GitHub ActionsSwipey Image Grids (with SVG) CS Visualized: CORS by Lydia Hallie https://dev.to/lydiahallie/cs-visualized-cors-5b8h What web dev hasn't struggled with CORS errors? Lydia explains here in great detail and with visual support the whys and wherefores of CORS and how to work around problems. Use Azure Functions, Azure Storage blobs, and Cosmos DB to copy images from public URLs by Dave Brock https://daveabrock.com/2020/11/25/images-azure-blobs-cosmos In this post, Dave shows how to work with Azure Storage blobs and Cosmos DB to copy images that are available over the public Internet. Build a Secure Desktop App with Electron Forge and React by Kilian Valkhof https://www.sitepoint.com/electron-forge-react-build-secure-desktop-app/?utm_source=rss Creating a cross-platform desktop app is easy thanks to Electron. Learn from Kilian how to create a secure desktop app using React, Electron and Electron Forge. Node.js API Authentication with JSON Web Tokens by Jay Krishna Reddy https://javascript.plainenglish.io/node-js-api-authentication-with-json-web-tokens-bb511f603723 In this article, Jay shows how to access the JSON web token (JWT) in Node.js and also to protect our routes with it. How To Build an SMTP Mail Server with Express, Node, and Gmail by Michael Rehnert https://daily.dev/blog/how-to-build-an-smtp-mail-server-with-express-node-and-gmail Michael shows in his tutorial how to use Node and Express to develop a mail server that uses Gmail for free transport via SMTP. Advanced Git Series 1/8: Creating the Perfect Commit in Git by Tobias Günther https://css-tricks.com/creating-the-perfect-commit-in-git/ In this series from 2021 on CSS-Tricks, Tobias goes into the most important aspects in the advanced handling of Git in a vivid way. Better commits, branching, collaboration, rebasing, cherry-picking … a good reminder. Learn how to use Git and GitHub in a team like a pro by Damian Demasi https://dev.to/colocodes/learn-how-to-use-git-and-github-in-a-team-like-a-pro-2dk7 This short two-part series by Damian clearly highlights the possibilities of collaboration with Git and the platform Github, which is usually neglected in other tutorials. How to Build Your First JavaScript GitHub Action by Bassem Dghaidi https://www.freecodecamp.org/news/build-your-first-javascript-github-action/ The automation possibilities of Github's Actions seem limitless. There is hardly a JavaScript workflow that cannot be mapped in some way. This is also due to the openness, because anyone can provide workflows. Bassem explains the basic system with examples. Getting the Gist of GitHub Actions by Brendon Smith https://gist.github.com/br3ndonland/f9c753eb27381f97336aa21b8d932be6 Like Bassem, Brendon shows some of the the basic principle of GitHub Actions, but expands on it with pro tips and workarounds when things get stuck. Swipey Image Grids (with SVG) by Cassie Evans https://www.cassie.codes/posts/swipey-image-grids/ If you're familiar with CSS animations, you might be interested in this post by Cassie, as she shows how to do it without CSS directly in SVG.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Include and provide JSON data in Hexo EJS Templates","subtitle":"... with a new Helper and an async function","date":"2023-06-27","updated":"2023-06-27","path":"post/Include-and-provide-JSON-data-in-Hexo-EJS-Templates/","permalink":"https://kiko.io/post/Include-and-provide-JSON-data-in-Hexo-EJS-Templates/","excerpt":"The three main components of a standard installation of the Static Site Generator Hexo are the template system EJS (Embedded JavaScript Templating), Markdown for the content and Stylus for the styles. In the template files are the three main tags for driving content: Scriptlet tag for control flow (no output) 123<% ... my JavaScript code to process data into the template%> Output a value as escaped HTML 1<%= myVariable %> Output of a raw value, usually in the form of a JavaScript function 1<%- myFunction() %> Hexo’s helper system is based on the latter. So you can include a JavaScript file in your template that makes use of the JS Helper in node_modules\\hexo\\lib\\plugins\\helper\\js.js as follows … 1<%- js('/js/dist/myFancyFunctions.js') %> … which will be rendered to: 1<script src="/js/dist//js/dist/qr-code-styling.js"></script> The ProblemSo far and short, so good … but I recently tried to use this way to include a JSON file whose data one of my scripts needed as startup options and I noticed that the above mentioned JS helper unfortunately takes care of the possibly missing file extension js. It doesn’t matter if you only pass the path to the file as a string or if all necessary attributes as an object.","keywords":"main components standard installation static site generator hexo template system ejs embedded javascript templating markdown content stylus styles files tags driving scriptlet tag control flow output 123<% code process data template%> escaped html 1<%= myvariable %> raw form function 1<%- myfunction hexos helper based include file makes js node_modules\\hexo\\lib\\plugins\\helper\\jsjs … js'/js/dist/myfancyfunctionsjs' rendered 1<script src="/js/dist//js/dist/qr-code-stylingjs"></script> problemso short good recently json scripts needed startup options noticed mentioned takes care possibly missing extension doesnt matter pass path string attributes object","text":"The three main components of a standard installation of the Static Site Generator Hexo are the template system EJS (Embedded JavaScript Templating), Markdown for the content and Stylus for the styles. In the template files are the three main tags for driving content: Scriptlet tag for control flow (no output) 123<% ... my JavaScript code to process data into the template%> Output a value as escaped HTML 1<%= myVariable %> Output of a raw value, usually in the form of a JavaScript function 1<%- myFunction() %> Hexo’s helper system is based on the latter. So you can include a JavaScript file in your template that makes use of the JS Helper in node_modules\\hexo\\lib\\plugins\\helper\\js.js as follows … 1<%- js('/js/dist/myFancyFunctions.js') %> … which will be rendered to: 1<script src="/js/dist//js/dist/qr-code-styling.js"></script> The ProblemSo far and short, so good … but I recently tried to use this way to include a JSON file whose data one of my scripts needed as startup options and I noticed that the above mentioned JS helper unfortunately takes care of the possibly missing file extension js. It doesn’t matter if you only pass the path to the file as a string or if all necessary attributes as an object. 1234<%- js({ src: 'js/dist/script-options.json', type: 'application/json'}) %> This code leads to the following wrong code … 12<script src="/js/dist//js/dist/qr-code-styling.json.js" type="application/json"></script> The JSON HelperSince Hexo’s developers went a bit over my/the target with the helper’s functionality, I had to build my own JSON helper, which is actually just a slightly customized copy of the original: themes\\landscape\\scripts\\json-helper.js12345678910111213141516171819202122232425262728const { htmlTag, url_for } = require('hexo-util');hexo.extend.helper.register('json', function(...args){ let result = '\\n'; args.flat(Infinity).forEach(item => { if (typeof item === 'string' || item instanceof String) { // args = String only let path = item; if (!path.endsWith('.json')) { path += '.json'; } result += `<script src="${url_for.call(this, path)}" type="application/json"></script>\\n`; } else { // args = Object -> Custom Attributes item.src = url_for.call(this, item.src); item.type = "application/json"; if (!item.src.endsWith('.json')) item.src += '.json'; result += htmlTag('script', { ...item }, '') + '\\n'; } }); return result;}); You can find the complete file here. With this its possible to reference the JSON like this: 1<%- json('js/dist/myOptions.json') %> Bring JSON data to lifeHowever, the helper only allowed me to load the file as such. What was still missing was the loading of the data in the JavaScript of the page itself. The easiest way to achieve that, was to perform a FETCH of the already referenced and loaded file in the SCRIPT block of the template as an immediately invoked async function: EJS File12345678910111213141516<%- js('js/dist/myFancyObjectLibrary.js') %><%- json({ src: 'js/dist/myOptions.json', id: 'my-options'}) %><script> (async () => { const response = await fetch(document.getElementById('my-options').src); const options = await response.json(); let obj = new myFancyObject(options); //... do something with the initialized object })();</script> Et voilà … Job done.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Templating","slug":"Templating","permalink":"https://kiko.io/tags/Templating/"}]},{"title":"Show pages meta data (JSON-LD) in Bottom Sheet","subtitle":"Visualize the generated meta code on your page in a sliding panel for review and information purposes","date":"2023-06-11","updated":"2023-06-11","path":"post/Show-pages-meta-data-JSON-LD-in-Bottom-Sheet/","permalink":"https://kiko.io/post/Show-pages-meta-data-JSON-LD-in-Bottom-Sheet/","excerpt":"A few months ago I introduced new meta data (JSON-LD) for the pages of this blog and also wrote about my implementation. Works also everything quite well … only the verification of the generated data was a bit cumbersome: Open DevTools for a page in Chrome. Search in the HEAD of the source code for the included script (“application/ld+json”) Copy out JSON-LD code Format JSON into VS code … and check Nothing for now and simply impossible on the smartphone, even if there would be a reasonable Chrome extension for displaying JSON-LD data, but it does not exist (yet). Another problem was that I use automatically generated Socal Media images for my articles, which are included in the JSON-LD, but do not appear anywhere in the page and thus were beyond my control. I simply wanted to display all the generated stuff. Since I’ve been a fan of the so-called bottom sheets since the first version of Google’s Material Design, I imagined a script that grabs the code embedded in the page and pushes a panel with all the data visualized there into the page from the bottom … and the whole act was easier than I thought.","keywords":"months ago introduced meta data json-ld pages blog wrote implementation works … verification generated bit cumbersome open devtools page chrome search head source code included script application/ld+json copy format json check simply impossible smartphone reasonable extension displaying exist problem automatically socal media images articles control wanted display stuff ive fan so-called bottom sheets version googles material design imagined grabs embedded pushes panel visualized act easier thought","text":"A few months ago I introduced new meta data (JSON-LD) for the pages of this blog and also wrote about my implementation. Works also everything quite well … only the verification of the generated data was a bit cumbersome: Open DevTools for a page in Chrome. Search in the HEAD of the source code for the included script (“application/ld+json”) Copy out JSON-LD code Format JSON into VS code … and check Nothing for now and simply impossible on the smartphone, even if there would be a reasonable Chrome extension for displaying JSON-LD data, but it does not exist (yet). Another problem was that I use automatically generated Socal Media images for my articles, which are included in the JSON-LD, but do not appear anywhere in the page and thus were beyond my control. I simply wanted to display all the generated stuff. Since I’ve been a fan of the so-called bottom sheets since the first version of Google’s Material Design, I imagined a script that grabs the code embedded in the page and pushes a panel with all the data visualized there into the page from the bottom … and the whole act was easier than I thought. The Bottom Sheet ComponentRecently I stumbled across a small but nice bottom sheet script that is based on jQuery but that I still use on this blog itself: downupPopup.js by Ali Dinçer. It has several settings and is just about 5 KB in size, if you add the CSS code. What’s nice about it is, that all the animations that make such a component stand out, are based on said CSS and are not jQuery-driven. The bottom sheet is based on a base HTML element with a required child element: 12345<div id="myElement1"> <div class="downupPopup-content"> Lorem ipsum dolor sit amet... </div></div> This is first initialized with the desired settings, with the script adding the necessary inline styles, and then you can open and close it: 1234$("#myElement1").downupPopup();$("#myElement1").downupPopup('open');$("#myElement1").downupPopup('close'); Now my solution should work on-the-fly and without a previously defined element in the HTML … and it should be reusable, because if I already include a bottom sheet component, then I wanted to use it for future occasions. For this I wrote myself a small manager that makes different uses possible with a single call. It has one function each for a specific bottom sheet dialog and beside it base variables and functions (base) to keep the infrastructure code of the former as small as possible: dialog.js123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869var dpDialog = { 'base': { 'element': null, 'content': null, 'options': { animation: "ease", duration: 400, radiusLeft: "6px", radiusRight: "6px", width: "100%", }, 'init': function(options) { let opt = {...dpDialog.base.options, ...options}; if ($("#dpElement").length === 0) { // create new dpDialog.base.element = $(` <div id="dpElement"> <div class="downupPopup-content"></div> </div>`); dpDialog.base.element.appendTo("body"); dpDialog.base.content = dpDialog.base.element.find(".downupPopup-content"); } else { // reset existing dpDialog.base.element.downupPopup("close"); dpDialog.base.content.empty(); } dpDialog.base.element.downupPopup(opt); }, 'show': function() { setTimeout(() => { dpDialog.base.element.downupPopup("open"); }, 100); } }, 'myFirstTest': function() { // INIT DIALOG dpDialog.base.init({ headerText: "Test", distance: 75 }); // ADD CONTENT let content = ` <section> <p>Lorem ipsum dolor sit amet...</p> </section> `; $(content).appendTo(dpDialog.base.content); // OPEN DIALOG dpDialog.base.show(); }, 'pageMeta': function() { // INIT DIALOG dpDialog.base.init({ headerText: "Page Meta", contentScroll: true, distance: 6 }); // ADD CONTENT // ... appending stuff to dpDialog.base.content // OPEN DIALOG dpDialog.base.show(); }};window.dialog = dpDialog; // make it globally available The base init function takes care of initializing the downupPopup component, including dynamically inserting the necessary HTML element and attaching the desired settings. show opens up the dialog, with a small time delay, to ensure that the content has already been inserted.Dialog functions in the example above are: myFirstTest and pageMeta. Calling one of the dialog functions is simple: 1<a href="javascript:dpDialog.myFirstTest()">Open Test Dialog</a> Try here: Open Test Dialog Problem with the original implementation solvedAli decided in his original implementation to apply the given settings only once to a popup element. Once initialized, it could not be reused with different settings. To avoid having to destroy an existing element before initializing a new one, which would have caused a massive timing problem due to the animation, I decided to fork his code and give him a pull request. You can find my script here, as long as Ali didn’t include the PR in his code: kristofzerbe/downupPopup.js The PageMeta DialogNow that I had my desired display option, it was time to bring the pageMeta dialog function to life. My first thought was to use a JSON-LD parser in JavaScript provided by json-ld.org, but this is not even quickly usable, since it validates the code to be parsed at runtime against schema.org and every of my calls failed with CORS warnings. Now I didn’t want to turn this into a PhD thesis, I just wanted to display my highly customized JSON-LD, so I worked out the function quite individually. The Code ItselfI wanted to show two things in the dialog: the code itself and a visual representation of it for a better overview. Getting the content for the code was really straight forward: dialog.js123456789101112131415161718192021222324... 'pageMeta': function() { // INIT DIALOG dpDialog.base.init({ headerText: "Page Meta", contentScroll: true, distance: 6 }); // ADD CONTENT // Grab the JSON-LD code from the page let json = JSON.parse($('script[type="application/ld+json"]').text()); // Create new dialog section for the code let secCode = $('<section></section>').appendTo(dpDialog.base.content); // Append header secCode.append('<h1>JSON-LD</h1>'); // Append formatted code als PRE element secCode.append('<pre class="json">' + syntaxHighlight(JSON.stringify(json, undefined, 2))) + '</pre>'; // OPEN DIALOG dpDialog.base.show(); }... Since I had taken care to compress my JSON-LD code to save space, I now needed to get it back into a readable format. Time saver was the following script, which I found on StackOverflow (pretty-print JSON using JavaScript): 123456789101112131415161718function syntaxHighlight(json) { json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); return json.replace(/("(\\\\u[a-zA-Z0-9]{4}|\\\\[^u]|[^\\\\"])*"(\\s*:)?|\\b(true|false|null)\\b|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)/g, function (match) { var cls = 'number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'key'; } else { cls = 'string'; } } else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; } return '<span class="' + cls + '">' + match + '</span>'; });} (Stylus format)12345678910111213pre.json font-family: 'Roboto Mono',monospace font-size: 13px .string color: #4271ae .number color: #4271ae .boolean color: #4271ae .null color: #ababab .key color: #c15353 The Visual RepresentationMy JSON-LD is hierarchically structured. Each page always has a PERSON block for information about me as a person, then an ORGANIZATION block about the “people” behind the blog (just me), then a WEBSITE block for the description of the website itself and a WEBPAGE block for a single page. Article pages like this, also have an ARTICLE block and the note pages have a BLOGPOSTING block. Therefore, it seemed logical to me to display the blocks as an accordion using the DETAILS element, with only the first one open at startup. To process the required HTML, I wrote a helper function for each block that returns a string literal template to which the calling code passes the necessary data. To save space, only one block is included in the example below. The others work similarly. You can see the complete code here and modify it for your purposes: dialog.js of the blog kiko.io. dialog.js123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354... 'pageMeta': function() { // INIT DIALOG dpDialog.base.init({ headerText: "Page Meta", contentScroll: true, distance: 6 }); // ADD CONTENT // Grab the JSON-LD code from the page let json = JSON.parse($('script[type="application/ld+json"]').text()); // Create new dialog section for the visual representation let secVisual = $('<section></section>').appendTo(dpDialog.base.content); // ... other blocks for the visual representation // Block WEBSITE function getWebSite(website, organization) { return ` <details> <summary>WebSite</summary> <div> <label>Name</label> <p>${website.name}</p> <label>Description</label> <p>${website.description}</p> <label>Language</label> <p>${website.inLanguage}</p> <label>Publisher</label> <p>${organization.name}</p> </div> </details> `; } // Get WebSite block from JSON let jWebSite = json["@graph"].filter(x => x["@type"] === "WebSite"); // Get referenced Publisher information (Organization) let jPublisher = json["@graph"].filter(x => x["@id"] === jWebSite[0].publisher["@id"]); // Get filled HTML from template helper function above let tWebSite = getWebSite(jWebSite[0], jPublisher[0]); // Append HTML to content secVisual.append($(tWebSite)); // ... other blocks for the visual representation // ... (Code stuff from above) // OPEN DIALOG dpDialog.base.show(); }... ConclusionIt was fun to add a new feature to the site, even more so because it helps me keep track of the meta data of each page myself. Here (or in the footer of each page) you can see the result: Open Page Meta for this article… More Information Ali Dinçer: downupPopup.jsKristof Zerbe: Fork from downupPopup.js (Make Popup Reusable, with PR)Google: Material Design 3 - Compontents, Bottom Sheets","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Meta","slug":"Meta","permalink":"https://kiko.io/tags/Meta/"},{"name":"JSON-LD","slug":"JSON-LD","permalink":"https://kiko.io/tags/JSON-LD/"}]},{"title":"Top 10 Pens of Jon Kantner","subtitle":null,"date":"2023-05-08","updated":"2023-05-08","path":"post/Top-10-Pens-of-Jon-Kantner/","permalink":"https://kiko.io/post/Top-10-Pens-of-Jon-Kantner/","excerpt":"A while ago I posted my favourite pens of 2022. Many of them came from the pen of John Kantner and that was reason enough for me to highlight his most beautiful works in a post. My “Best of Jon Kantner” selection is focused on the usefulness for UI’s to be created, i.e. all these things I urgently need to try out or one or the other will end up in one of my projects.","keywords":"ago posted favourite pens pen john kantner reason highlight beautiful works post jon selection focused usefulness uis created things urgently end projects","text":"A while ago I posted my favourite pens of 2022. Many of them came from the pen of John Kantner and that was reason enough for me to highlight his most beautiful works in a post. My “Best of Jon Kantner” selection is focused on the usefulness for UI’s to be created, i.e. all these things I urgently need to try out or one or the other will end up in one of my projects. 1. Card-Like Menu 2. Side Navigation 3. Lotsa Notifications 4. Search Input Caret Jump 5. Sliding Stepper 6. Passcode With Sliding Cursor 7. Animated Star Rating 8. Range Sliders With a Rolling Counter 9. Colorful Theme Switch 10. Animated Upload Modal As I said, this is the list of my personal favorite works by Jon. I can only recommend everyone to check out his work on Codepen. He really is a UI rockstar …","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"UI","slug":"UI","permalink":"https://kiko.io/tags/UI/"}]},{"title":"Discoveries #24 - JavaScript & UI","subtitle":null,"series":"Discoveries","date":"2023-04-28","updated":"2023-04-28","path":"post/Discoveries-24-JavaScript-UI/","permalink":"https://kiko.io/post/Discoveries-24-JavaScript-UI/","excerpt":"I’m such a UI person. It’s a blast for me to discover and try out new interface components on the web and for the web. Simplicity and a tidy text desert is not my thing. Here are a few JavaScript UI gems I found the other day for you… Frontle - BottomSheetdownupPopup.jsPikadayAdd to Calendar ButtonTippy.jsResponsive Full Page Scrolllucafalasco/scroll-snapBgzy.jsScrollyVideo.jsCal-Heatmap","keywords":"im ui person blast discover interface components web simplicity tidy text desert thing javascript gems found day you… frontle - bottomsheetdownuppopupjspikadayadd calendar buttontippyjsresponsive full page scrolllucafalasco/scroll-snapbgzyjsscrollyvideojscal-heatmap","text":"I’m such a UI person. It’s a blast for me to discover and try out new interface components on the web and for the web. Simplicity and a tidy text desert is not my thing. Here are a few JavaScript UI gems I found the other day for you… Frontle - BottomSheetdownupPopup.jsPikadayAdd to Calendar ButtonTippy.jsResponsive Full Page Scrolllucafalasco/scroll-snapBgzy.jsScrollyVideo.jsCal-Heatmap Frontle - BottomSheet by Frontl (HyeongJun Yun) https://github.com/Frontle-Foundation/BottomSheet BottomSheet is part of the Frontl multi-platform SPA framework from South Korea and is a vanilla JavaScript implementation of the Android Bottom Sheet to show options or settings. Very neat and useful for Web Apps. downupPopup.js by Ali Dinçer https://github.com/ali-dincer/downupPopup.js Ali’s downupPopup is a wonderful UI element for replacing boring dialogs or showing additional information in a sliding panel, similar to the the previous BottomSheet. It masters HTML, forms and can be shown with a duration and full screen. Pikaday by David Bushell and Ramiro Rikkert https://github.com/Pikaday/Pikaday This date picker is not the fanciest one, but it is very lightweight, has no dependencies and is written in plain JavaScript. The ease of use and the ability to style it however you want, makes it a great little thing. Add to Calendar Button by Jens Kürschner https://add-to-calendar-button.com/ Letting the user select a date is just the first part of a process when it comes to making appointments. In the second part, the appointment must be added to the user's own calendar. This button makes it very easy in terms of Outlook, Google Calendar, Yahoo or as an ICS file. Tippy.js by James, somewhere from Australia https://atomiks.github.io/tippyjs/ Tooltip libraries really exist a lot. But Tiffy, a side project of Floating UI (now Popper), offers so many possibilities that I replace existing ones with it. Animations, Themes, Add-Ons, Plugins, and much more. Even SVG's are supported. Awesome! Responsive Full Page Scroll by Fabian Graßl https://github.com/fabeat/responsive-fullpage-scroll If you throw this library on a bit of HTML you get a wonderful a full screen scrolling page that can be activated and deactivated using a media query. Perfect for marketing pages or photo slideshows. lucafalasco/scroll-snap by Luca Falasco https://github.com/lucafalasco/scroll-snap This library helps to snap a page when user stops scrolling. It is build on top of the CSS feature scroll-snap, but offers a customizable configuration and a consistent cross browser behaviour. Bgzy.js by Nino Camdzic https://ninocamdzic.github.io/bgzy/# Visually appealing backgrounds are sometimes the salt in the soup. Of course, it depends on the photo and so that it does not look boring in the long run, Nico thought, then I'll change them in adjustable time periods with JavaScript. Great idea and nice implementation. ScrollyVideo.js by Daniel Kao https://scrollyvideo.js.org/ Parallax effects when scrolling through a website are well known by now. But the fact that you can use a video for this was new to me. What an idea! And how cool it looks in the end… Cal-Heatmap by Wan from Dubai https://cal-heatmap.com/ Anyone who has ever been on GitHub knows the heatmaps that show when and how often commits have taken place. But this kind of chart can be used for other things, Wan probably thought and made a library out of it.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Breton Presets for Lightroom","subtitle":null,"series":"Lightroom Presets","date":"2023-03-10","updated":"2023-03-10","path":"post/Breton-Presets-for-Lightroom/","permalink":"https://kiko.io/post/Breton-Presets-for-Lightroom/","excerpt":"‘Le Bretagne’ (Brittany) is one of the most beautiful and historic parts of Europe and always worth a visit. It combines the sometimes rugged English flair with the art of living of France, both scenically and in the architecture and the way people live there. My wife likes all things English and I am a friend of the french ‘Savoir Vivre’, and we were able to combine the two beautifully on a trip to Saint-Malo and Jersey. It was a photographers dream…","keywords":"le bretagne brittany beautiful historic parts europe worth visit combines rugged english flair art living france scenically architecture people live wife likes things friend french savoir vivre combine beautifully trip saint-malo jersey photographers dream…","text":"‘Le Bretagne’ (Brittany) is one of the most beautiful and historic parts of Europe and always worth a visit. It combines the sometimes rugged English flair with the art of living of France, both scenically and in the architecture and the way people live there. My wife likes all things English and I am a friend of the french ‘Savoir Vivre’, and we were able to combine the two beautifully on a trip to Saint-Malo and Jersey. It was a photographers dream… Breton GlowThe sunsets off Saint-Malo are wonderful. The water of the English Channel glows and in the sky the silhuettes of the paragliders stand out, pulling the sportsmen over the rippling water. You can sit on the waterfront for hours and still not get enough of it. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-0di102\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Bretagne Glow.xmp Breton SundownHaving good and clear weather here, as in the British Isles, is not a given. The evenings are all the more beautiful when it is like this and the small islands off the coast are bathed in a warm orange. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-tcx49h\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Bretagne Sundown.xmp Breton ColorI love colors, but rarely does the camera manage to capture that magic the way I feel it. And Brittany has beautiful colors. Be it the famous Mont Saint-Michel or a cornfield … :) var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-ubla0i\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Bretagne Color.xmp Breton Sea CircleOysters from Cancale are not only famous in the region, but far beyond. A product that tastes just like the sea from which it comes looks: Awesome. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-2fsy5v\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Bretagne Sea Circle.xmp Breton BeachThe beach in Saint-Malo is 3 kilometers long and it is fascinating to observe how one of the highest tides in Europe swallows it in no time and later releases it cleaned. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-b7qbni\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Bretagne Beach.xmp","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Presets","slug":"Presets","permalink":"https://kiko.io/tags/Presets/"}]},{"title":"Discoveries #23 - UI/CSS","subtitle":null,"series":"Discoveries","date":"2023-02-12","updated":"2023-02-12","path":"post/Discoveries-23-UI-CSS/","permalink":"https://kiko.io/post/Discoveries-23-UI-CSS/","excerpt":"As a visual person, I’m always thrilled when I come across small but subtle tips, tricks or even entire solutions that lift my understanding of what can be done with CSS to new heights. Of course, this month :has() is once again one of them, but also once again contributions from Bramus van Damme and Ahmad Shadeed, whose posts I read without exception because they are both so good at what they do. Tree views in CSSScroll Shadows With JavaScriptCSS Mirror Editing in Edge DevTools for VS CodePrevent Scroll Chaining With Overscroll BehaviorDisplay content in the title bar - Microsoft Edge DevelopmentThe large, small, and dynamic viewport unitsAn Interactive Guide to Flexbox in CSSFlexbox Dynamic Line SeparatorStyle a parent element based on its number of children using CSS :has():has(): the family selector","keywords":"visual person im thrilled small subtle tips tricks entire solutions lift understanding css heights month contributions bramus van damme ahmad shadeed posts read exception good tree views cssscroll shadows javascriptcss mirror editing edge devtools codeprevent scroll chaining overscroll behaviordisplay content title bar - microsoft developmentthe large dynamic viewport unitsan interactive guide flexbox cssflexbox line separatorstyle parent element based number children hashas family selector","text":"As a visual person, I’m always thrilled when I come across small but subtle tips, tricks or even entire solutions that lift my understanding of what can be done with CSS to new heights. Of course, this month :has() is once again one of them, but also once again contributions from Bramus van Damme and Ahmad Shadeed, whose posts I read without exception because they are both so good at what they do. Tree views in CSSScroll Shadows With JavaScriptCSS Mirror Editing in Edge DevTools for VS CodePrevent Scroll Chaining With Overscroll BehaviorDisplay content in the title bar - Microsoft Edge DevelopmentThe large, small, and dynamic viewport unitsAn Interactive Guide to Flexbox in CSSFlexbox Dynamic Line SeparatorStyle a parent element based on its number of children using CSS :has():has(): the family selector Tree views in CSS by Kate Rose Morley https://iamkate.com/code/tree-views/ Kate shows us how to create a tree view as collapsible list, created using only html and css, without the need for JavaScript Scroll Shadows With JavaScript by Chris Coyier https://css-tricks.com/scroll-shadows-with-javascript/ A good scrollable design shows the user if he can scroll further or not. Chris has an approach on that with pure CSS. CSS Mirror Editing in Edge DevTools for VS Code by Christian Heilmann https://christianheilmann.com/2021/09/16/css-mirror-editing-in-edge-devtools-for-vs-code/ How often do you fiddle around with CSS in Chrome's DevTools and copy the stuff back to your code? Christian shows how Mirror Editing works. Prevent Scroll Chaining With Overscroll Behavior by Ahmad Shadeed https://ishadeed.com/article/prevent-scroll-chaining-overscroll-behavior/ Dealing with scroll boundaries when you have many scrolling boxes on a page is a mess, until you have read Ahmad's advice regarding the use of 'overscroll-behavior' Display content in the title bar - Microsoft Edge Development by Microsoft Learn https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/window-controls-overlay In PWAs, at least on the desktop, a lot of space is wasted with the title bar of the window. The use of 'display_override' should change that. The large, small, and dynamic viewport units by Bramus Van Damme https://web.dev/viewport-units/ The most used device on the Internet has long been the smartphone, but the visible area is trimmed by the browsers there from the necessary dynamic toolbars. To address this, there are new size units. An Interactive Guide to Flexbox in CSS by Josh Comeau https://www.joshwcomeau.com/css/interactive-guide-to-flexbox/ There are plenty of Flexbox tutorials, cheat sheets and generators, but Josh turns it into an interactive learning lesson. Very memorable. Flexbox Dynamic Line Separator by Ahmad Shadeed https://ishadeed.com/article/flexbox-separator/ Flexbox again and Ahmad again … If you need separator lines between boxes for different devices, here's how the can be done nice and easy. Style a parent element based on its number of children using CSS :has() by Bramus Van Damme https://www.bram.us/2022/11/17/style-a-parent-element-based-on-its-number-of-children-using-css-has/ :has() is the hottest kid in town right now, because it allows the long-cherished dream of many web developers to style a parent element depending on his children. Bramus shows how… :has(): the family selector by Jhey Tompkins https://developer.chrome.com/blog/has-m105/ As :has() is so hot, it's good to have another resource talking about. Jhey has collected so many examples that hardly any questions remain open.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Provide Blog Metadata via JSON-LD","subtitle":"Centralization of a website's schema.org data in the HEAD instead of everywhere in the HTML","series":"A New Blog","date":"2023-02-09","updated":"2023-02-09","path":"post/Provide-Blog-Metadata-via-JSON-LD/","permalink":"https://kiko.io/post/Provide-Blog-Metadata-via-JSON-LD/","excerpt":"Chris Coyier’s post “Open Graph Blues“ got me thinking that my blog’s metadata, which are used by Google among others to index my pages, aren’t really at the cutting edge anymore. I took the markup of the individual elements of the pages via schema.org Microdata attributes from the standard Hexo template years ago and always adjusted it by value, but never questioned that there are more modern variants to provide the metadata. It’s Ok for Google to use Microdata attributes, but the HTML code of my templates is getting more and more opaque, because next to these stick to the tags also those for the Indieweb, classes for CSS and last but not least those for the own indexing via Pagefind. There becomes from a simple 1234<article> <h1>Title of my latest blog post</h1> <p> ... </p></article> quickly becomes a … 123456<article class="article-type-post h-entry" data-pagefind-body="" data-pagefind-meta="type:Article" itemscope itemprop="blogPost"> <h1 class="article-title p-name" itemprop="name">Title of my latest blog post</h1> <p> ... </p></article> Lots of textual overhead and the hardest part is maintaining it over the long term. Better would be a complete search engine description in the header of a page, where also the other meta information is available. In one place and not spread all over the HTML code. JSON-LD to the rescue…","keywords":"chris coyiers post open graph blues thinking blogs metadata google index pages arent cutting edge anymore markup individual elements schemaorg microdata attributes standard hexo template years ago adjusted questioned modern variants provide html code templates opaque stick tags indieweb classes css indexing pagefind simple 1234<article> <h1>title latest blog post</h1> <p> </p></article> quickly … 123456<article class="article-type-post h-entry" data-pagefind-body="" data-pagefind-meta="typearticle" itemscope itemprop="blogpost"> <h1 class="article-title p-name" itemprop="name">title lots textual overhead hardest part maintaining long term complete search engine description header page meta information place spread json-ld rescue…","text":"Chris Coyier’s post “Open Graph Blues“ got me thinking that my blog’s metadata, which are used by Google among others to index my pages, aren’t really at the cutting edge anymore. I took the markup of the individual elements of the pages via schema.org Microdata attributes from the standard Hexo template years ago and always adjusted it by value, but never questioned that there are more modern variants to provide the metadata. It’s Ok for Google to use Microdata attributes, but the HTML code of my templates is getting more and more opaque, because next to these stick to the tags also those for the Indieweb, classes for CSS and last but not least those for the own indexing via Pagefind. There becomes from a simple 1234<article> <h1>Title of my latest blog post</h1> <p> ... </p></article> quickly becomes a … 123456<article class="article-type-post h-entry" data-pagefind-body="" data-pagefind-meta="type:Article" itemscope itemprop="blogPost"> <h1 class="article-title p-name" itemprop="name">Title of my latest blog post</h1> <p> ... </p></article> Lots of textual overhead and the hardest part is maintaining it over the long term. Better would be a complete search engine description in the header of a page, where also the other meta information is available. In one place and not spread all over the HTML code. JSON-LD to the rescue… Structured Meta DataGoogle has published tons of information in its Search Central on how to place metadata on your page to be found more easily in the index. You can also see that they are maintained by the update date of individual pages, for example “Last updated 2023-01-26 UTC“. End of last week. That’s up to date, fine. Of course, they also show how to use Microdata, but recommended is the use of JSON-LD, a structured and centralized inclusion of the required information via a SCRIPT tag in the header of the page. Thereby information about the website in general, the author, the organization behind it and the actual article page can be combined separately in one piece of JSON-LD code. Google’s solution is based on schema.org, but they have picked only what is necessary for them, which means: they deal only with a subset of the schema.org types. Since it is somewhat cumbersome to write correct JSON-LD by hand, there are of course online editors for it, e.g. within the web code tools or Merkle. But these generators unfortunately do not map all the possibilities and useful entries, so they can only generate general templates to be elaborated. Moreover, you can use the JSON-LD code from this article as a basis for your solution, because it covers the most important aspects. In the following I first describe the general content of the individual JSON-LD blocks and then how to assemble them, so that it makes sense for the search engine. AuthorFirst of all, this code is about me myself and I… 123456789101112{ "@type": "Person", "@id": "https://kiko.io/#person", "name": "Kristof Zerbe", "url": "https://kiko.io/about", "image": "https://kiko.io/images/kristof-zerbe.png", "sameAs": [ "https://indieweb.social/@kiko", "https://github.com/kristofzerbe", "https://500px.com/p/kikon" ] } It is advisable to include so called Node Identifiers (@id) in order to reuse certain information later on as a reference and prevent repeating data. These identifiers are canonical URL/URI representations. OrganizationMost blogs are run by individuals and not necessarily by organizations, so you might think this area would not be interesting, but it is for a reason: only here you can deposit the link to a logo of your blog, which can then be displayed in the search. 1234567{ "@type": "Organization", "@id": "https://kiko.io/#organization", "name": "kiko.io", "url": "https://kiko.io", "logo": "https://kiko.io/images/apple-touch-icon.png"} WebsiteThe JSON-LD block related to this website itself looks like this: 12345678910111213141516171819{ "@type": "WebSite", "@id": "https://kiko.io/#website", "url": "https://kiko.io", "name": "kiko.io", "description": "Blog about memorable tech stuff by Kristof Zerbe", "inLanguage": "en-US", "publisher": { "@id": "https://kiko.io/#organization" }, "potentialAction": { "@type": "SearchAction", "target": { "@type": "EntryPoint", "urlTemplate": "https://kiko.io/search/?q={searchTerm}" }, "query-input": "required name=searchTerm" }} Remarkable in this block is the potentialAction, which specifies the possibility to let the search engine (Google, whatelse) integrate a Sitelinks Search Box, a search box inside the result list, as described here. There is a shorthand format for this and some generators like Merkle are using it, but it is not recommended, because it’s non-standard. (Page) ImagesI use my own photographs on every page as header images and to provide some additional information on these, there is a JSON-LD block for the image I use. 1234567891011121314{ "@type": "ImageObject", "@id": "https://kiko.io#photo/D70_9216", "caption": "Broken Onion", "url": "https://kiko.io/photos/normal/D70_9216.jpg", "contentUrl": "https://kiko.io/photos/normal/D70_9216.jpg", "license": "https://creativecommons.org/licenses/by-sa/4.0/", "acquireLicensePage": "https://kiko.io/photos", "creditText": "Kristof Zerbe", "copyrightNotice": "Kristof Zerbe (CC BY-SA 4.0)", "creator": { "@id": "https://kiko.io/#person" }} For better recognition of my posts (currently articles only), I generate a special image for each post for the social media platforms. It is based on the photo associated with the post and includes it’s title and subtitle in addition to the logo. How I generate these things can be read in my post Generate Social Media Images Automatically. For this image, there is a second JSON-LD block that I can reference later on: 123456789{ "@type": "ImageObject", "@id": "https://kiko.io#image/Provide-Blog-Metadata-via-JSON-LD", "url": "https://kiko.io/images/social-media/Provide-Blog-Metadata-via-JSON-LD.png", "contentUrl": "https://kiko.io/images/social-media/Provide-Blog-Metadata-via-JSON-LD.png", "creator": { "@id": "https://kiko.io/#person" }} WebpageThe previously described JSON-LD blocks are basically just a preparation for the description of the individual pages that will be indexed by the search engine. The following block now describes a page itself and includes the previously described blocks by referencing the @id: 1234567891011121314151617{ "@type": "WebPage", "@id": "https://kiko.io/post/Provide-Blog-Metadata-via-JSON-LD", "url": "https://kiko.io/post/Provide-Blog-Metadata-via-JSON-LD", "name": "Provide Blog Metadata via JSON-LD", "isPartOf": { "@id": "https://kiko.io/#website" }, "description": "Centralization of a website's schema.org data in the HEAD instead of everywhere in the HTML", "inLanguage": "en-US", "primaryImageOfPage": { "@id": "https://kiko.io#photo/D70_9216" }, "image": { "@id": "https://kiko.io#photo/D70_9216" }} ArticleIn addition to the previous description of a web page, the following block is a more detailed description of the page as an article or blog post. 12345678910111213141516171819{ "@type": "Article", "@id": "https://kiko.io/post/Provide-Blog-Metadata-via-JSON-LD#article", "mainEntityOfPage": { "@id": "https://kiko.io/post/Provide-Blog-Metadata-via-JSON-LD" }, "headline": "Provide Blog Metadata via JSON-LD", "datePublished": "2023-02-02T12:00:00+00:00", "dateModified": "2023-02-02T12:00:00+00:00", "image": { "@id": "https://kiko.io#image/Provide-Blog-Metadata-via-JSON-LD" }, "author": { "@id": "https://kiko.io/#person" }, "publisher": { "@id": "https://kiko.io/#organization" }} As described earlier, the identifiers defined in the other blocks are used for referencing the author and the publisher, as I did it in the block website. At first glance, this information appears to be duplicated and I am currently not sure if it is really needed.mainEntityOfPage makes clear, that this article is the main entity for that web page.The image of the article will be the social media image defined in the second ImageObject block. Assembling the blocksGenerally, JSON-LD data is included in a page via a script tag. You can either output each individual block separately or together within a so called graph. In both cases, the leading specification of the context is necessary. Separate 12345678<script type="application/ld+json"> { "@context": "http://schema.org/", ... rest of the block }</script><script type="application/ld+json"> ... </script>... Together 123456789<script type="application/ld+json"> { "@context": "http://schema.org/", "@graph": [ ... all needed blocks in hierarchy ] }</script> Since this is probably the most common and also the most space-saving way, I have chosen the graph. When arranging the blocks it is useful to keep the hierarchy, from specific to general. Here is a schematic example of an article: 123456789101112131415<script type="application/ld+json"> { "@context": "http://schema.org/", "@graph": [ { ... Article }, { ... WebPage }, { ... ImageObject (Social Media Image) }, { ... ImageObject (Photo) }, { ... WebSite }, { ... Organization }, { ... Person } ] }</script> On all pages that are not an article, of course, the Article and the ImageObject (Social Media Image) blocks are not necessary. Here is the sample for an ordinary page: 12345678910111213<script type="application/ld+json"> { "@context": "http://schema.org/", "@graph": [ { ... WebPage }, { ... ImageObject (Photo) }, { ... WebSite }, { ... Organization }, { ... Person } ] }</script> GenerationSince my blog is based on SSG Hexo, I have all the data and capabilities to have the JSON-LD data of a page automatically generated. I don`t want to go into too much depth here about how I implemented this, but in general there is an EJS file for each block that renders the required JSON-LD code via the available configuration and page data stored and passed in a META object. Through various wrappers, these are then included in the head.ejs. I have currently three of them: json-ld-page.ejs … for all pages, except the other two below json-ld-article.ejs … for articles (normal kiko.io Posts) json-ld-blogposting.ejs … for notes (see kiko.io Notes) json-ld-article.ejs12345678910111213{ "@context": "http://schema.org/", "@graph": [ <%- partial('_partial/meta/_article') %>, <%- partial('_partial/meta/_webpage') %>, <%- partial('_partial/meta/_image') %>, <%- partial('_partial/meta/_photo') %>, <%- partial('_partial/meta/_website') %>, <%- partial('_partial/meta/_organization') %>, <%- partial('_partial/meta/_author') %> ]} head.ejs12345678<%#!-- JSON-LD (schema.org) for Google --%><% let jsonPartial = "json-ld-" + meta.type; let jsonLD = partial('_partial/meta/' + jsonPartial, { meta: meta }); jsonLD = JSON.stringify(JSON.parse(jsonLD));%><script type="application/ld+json"><%- jsonLD %></script> The last line in the JavaScript ensures, that the produced JSON is compacted to one single line … easy, by converting the string into an object and back to a string. Test the JSON-LDWhen you have everything together, it is advisable to test the resulting code. Schema.org offers such a tool at https://validator.schema.org/. In addition, there is the Google Rich Results Test, which validates your code against the partially specific implementation for their own search engine. ConclusionIt may be a few bytes more that are delivered to the user in the browser or the search engine bot, but the advantage is, that all the information describing the page is stored in one place in the header of the page and nothing is scattered all over the HTML anymore. Maintenance of both the code and the meta data is made much easier as a result. More Information Google Search Central: Introduction to structured data markup in Google SearchPatrick Coombe and Craig Mount: Steal Our JSON-LDAndrew Welch: Annotated JSON-LD Structured Data ExamplesAlberto Carniel: Schema markup and structured data ultimate guide (JSON-LD)Brian Gorman: An SEO’s Guide to Writing Structured Data (JSON-LD)Merkle: Schema Markup Generator (JSON-LD)webcode.tools: Generators > Structured Data","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Search","slug":"Search","permalink":"https://kiko.io/tags/Search/"},{"name":"JSON-LD","slug":"JSON-LD","permalink":"https://kiko.io/tags/JSON-LD/"}]},{"title":"Radio Garden","subtitle":"Free, registration-free and simply cool - Travel through the radio stations of the world with Radio Garden","series":"Golem","date":"2023-02-01","updated":"2023-02-01","path":"post/Radio-Garden/","permalink":"https://kiko.io/post/Radio-Garden/","excerpt":"In the age of streaming services, radio may seem out of date, but it still surrounds us constantly, even if we often hardly notice it - in the car, on the public transport, at work or simply at home in the kitchen. One strength of the old medium is that it presents us with music we haven’t heard before, away from our playlists on Spotify or iTunes. The easiest way to rediscover new artists or entire genres of music is to pick a radio station at random. It’s precisely this lack of control, the being at the mercy, the sometimes unpredictable that makes the medium so appealing to many - and with digital streaming, many of the world’s stations are just a tap away. As is so often the case in the modern world, it’s the oversupply that leaves some confused and frustrated. You first have to be able to pick out what you might like from the gigantic haystack of options. For this purpose, there are little helpers that more or less independently suggest what you might listen to. One of them stands out from the crowd because, on the one hand, it does not pursue commercial interests and, on the other hand, it approaches the station search very intuitively: Radio Garden (https://radio.garden).","keywords":"age streaming services radio date surrounds constantly notice - car public transport work simply home kitchen strength medium presents music havent heard playlists spotify itunes easiest rediscover artists entire genres pick station random precisely lack control mercy unpredictable makes appealing digital worlds stations tap case modern world oversupply leaves confused frustrated gigantic haystack options purpose helpers independently suggest listen stands crowd hand pursue commercial interests approaches search intuitively garden (https://radio.garden).","text":"In the age of streaming services, radio may seem out of date, but it still surrounds us constantly, even if we often hardly notice it - in the car, on the public transport, at work or simply at home in the kitchen. One strength of the old medium is that it presents us with music we haven’t heard before, away from our playlists on Spotify or iTunes. The easiest way to rediscover new artists or entire genres of music is to pick a radio station at random. It’s precisely this lack of control, the being at the mercy, the sometimes unpredictable that makes the medium so appealing to many - and with digital streaming, many of the world’s stations are just a tap away. As is so often the case in the modern world, it’s the oversupply that leaves some confused and frustrated. You first have to be able to pick out what you might like from the gigantic haystack of options. For this purpose, there are little helpers that more or less independently suggest what you might listen to. One of them stands out from the crowd because, on the one hand, it does not pursue commercial interests and, on the other hand, it approaches the station search very intuitively: Radio Garden (https://radio.garden). Indian esotericism, Estonian metalThe tool, which works very well in any modern browser, but is also available as an Android or iOS app, initially shows nothing more than an interactive globe on which tens of thousands of green dots whiz by under a round target cursor when you move the map back and forth or zoom into the world. If you set your sights on one of the green dots, the station for that city is loaded and played. If there are several stations in one place, the dot appears larger and you are also offered a station list. This is so intuitive that it instantly awakens the spirit of discovery and in a few minutes you can listen to Japanese, Australian or Peruvian radio stations and decide whether to add them to your list of favorites. Using the search function, you can find not only your local, familiar stations (if they offer an Internet stream), but also thematic stations. The directory even includes some US police stations that stream their radio traffic on the Internet. Radio Garden grew out of a scholarly project by the Netherland Institute for Sound and Vision in Hilversum, the Netherlands, which between 2013 and 2016 studied what radio sounds like in other parts of the world and what borders, different cultural identities, and encounters do to the medium. The British Jonathan Puckey and his studio played a key role in the project and its technical implementation. In several iteration stages, this resulted in the web app radio.garden, which Puckey is still in charge of today. Instead of maps, the team from Amsterdam used satellite images from the beginning to illustrate that radio signals have always had the power to cross borders. Radio Garden is free, and you don’t need to register, log in, or even provide an email address. Nothing. Open up, select a station, listen to the radio. tns({ container: \"#image-slide-ks1oye\", items: 1, slideBy: \"page\", controls: false, nav: true }); Unfortunately, there are a few regions in the world that have limited access to the radio.garden website or apps. For example, in the replies to a Mastodon post on the topic, there were some comments from people in the UK who could not access a station outside their island. This problem seems to have a licensing background and thus be a Brexit impact, but there are unfortunately no official statements on this. Similarly in Turkey, where the service was discontinued after the Radio and Television Supreme Council there ordered the operator to make license payments, according to Wikipedia. The only way around these restrictions is to use a VPN in another country. It’s a lot of fun to roam around the globe and try to follow an Indonesian news broadcast, guess which products the advertisements of a Greek station are trying to sell, let yourself be carried away to other worlds by the spherical sounds of an Indian esoteric station, or really shake your hair to the beat of the hard metal sounds of an Estonian rock station. Radio Garden is a very exciting project, wonderfully presented.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Audio","slug":"Audio","permalink":"https://kiko.io/tags/Audio/"}]},{"title":"Pagefind UI and URL Parameters","subtitle":null,"series":"A New Blog","date":"2023-01-31","updated":"2023-02-03","path":"post/Pagefind-UI-and-URL-Parameters/","permalink":"https://kiko.io/post/Pagefind-UI-and-URL-Parameters/","excerpt":"UPDATE: Parts of the original post are outdated, as Pagefind DOES offer a way to preset a search string, which just hasn’t been documented yet … \\o/ … see below. A couple of days ago I wrote about my attempt to integrate Pagefind in my blog. In the meantime, I further refined the indexing by excluding more content areas and adding more for the metadata to make the search results even better. But one thing was still missing: controlling the search via url parameters, so that you can actually consider the page as a search page. I came across this in a post about the sense and nonsense of Open Graph attributes and other search engine related metadata nowadays. Google, for example, likes to use JSON-LD and when describing the site you can define a search page which then makes it easier to search the site directly via Google … see Sitelinks search box (WebSite) structured data In my implementation, I decided to adapt the Pagefind UI for myself instead of developing everything from scratch via JavaScript. There are always some limitations with pre-built solutions, but I want to show here that they are actually none for the inclusion of a url parameter.","keywords":"update parts original post outdated pagefind offer preset search string hasnt documented … \\o/ couple days ago wrote attempt integrate blog meantime refined indexing excluding content areas adding metadata make results thing missing controlling url parameters page sense nonsense open graph attributes engine related nowadays google likes json-ld describing site define makes easier directly sitelinks box website structured data implementation decided adapt ui developing scratch javascript limitations pre-built solutions show inclusion parameter","text":"UPDATE: Parts of the original post are outdated, as Pagefind DOES offer a way to preset a search string, which just hasn’t been documented yet … \\o/ … see below. A couple of days ago I wrote about my attempt to integrate Pagefind in my blog. In the meantime, I further refined the indexing by excluding more content areas and adding more for the metadata to make the search results even better. But one thing was still missing: controlling the search via url parameters, so that you can actually consider the page as a search page. I came across this in a post about the sense and nonsense of Open Graph attributes and other search engine related metadata nowadays. Google, for example, likes to use JSON-LD and when describing the site you can define a search page which then makes it easier to search the site directly via Google … see Sitelinks search box (WebSite) structured data In my implementation, I decided to adapt the Pagefind UI for myself instead of developing everything from scratch via JavaScript. There are always some limitations with pre-built solutions, but I want to show here that they are actually none for the inclusion of a url parameter. My Pagefind search page is accessible at /search and therefore it’s easy to provide with parameters, f.e. /search/?q=test. Retrieving them on the page via JavaScript is no rocket science either: Search Page12345678910111213<script> // get value search parameter const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const searchString = urlParams.get("q"); // initialize Pagefind UI window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({ element: "#search" }); });</script> Now Pagefind does not offer the possibility to initialize the search on the page already with a certain value, which would be the easiest way. You can only insert the value supplied via the URL parameter into the initialized INPUT field **afterwards** and ensure that the search is triggered with it. Unfortunately, Pagefind also does not provide a callback method to do things after successful initialization. So, my implementation needs a \"guard\" that kicks in as soon as the INPUT field is ready for a search string to be entered. For this I use the following small function called ``waitForElm``, which uses JS's ``MutationAbserver`` and is located in my *tools.js* file. It triggers a Promise as soon as an element is available on the page. Update As I learned after creating an issue (#214) in Pagefind’s GitHub repo, there IS a way to preset the incoming search string by using the method triggerSearch, but as Liam pointed out it is not yet documented. But I still need the following function called waitForElm to set the focus into the created INPUT … but there is also an existing issue (#121) for this focus feature, so let’s see how long I need the function at all. tools.js12345678910111213141516171819function waitForElm(selector) { return new Promise((resolve) => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver((mutations) => { if (document.querySelector(selector)) { resolve(document.querySelector(selector)); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true, }); });} With this function and the name of the INPUT field that Pagefind inserts into the ``#search`` wrapper during initialization, the URL parameter can now be set. Since Pagefind already shows results when typing the first characters, the easiest way to trigger the search after setting the value is by dispatching the ``input`` event. Lets see how to implement Pagefind’s triggerSearch function, which is automatically delayed until the search is loaded and ready, if there is an incoming search string: Search Page123456789101112131415161718192021<script> // get value search parameter const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const searchString = urlParams.get("q"); // initialize Pagefind UI window.addEventListener('DOMContentLoaded', (event) => { let pagefind = new PagefindUI({ element: "#search" }); if (searchString) { pagefind.triggerSearch(searchString); } }); // setting the focus into the generated INPUT field as it appears waitForElm(".pagefind-ui__search-input").then((elm) => { elm.focus(); }); </script> You can try my solution here: /search/?q=pagefind","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"},{"name":"Search","slug":"Search","permalink":"https://kiko.io/tags/Search/"}]},{"title":"Integration of Pagefind in Hexo","subtitle":"Adding a low-bandwidth local search to a static Hexo-powered website","series":"A New Blog","date":"2023-01-19","updated":"2023-01-19","path":"post/Integration-of-Pagefind-in-Hexo/","permalink":"https://kiko.io/post/Integration-of-Pagefind-in-Hexo/","excerpt":"From the beginning of this blog I wanted to provide some kind of full text search in order to give my users the ability to find stuff by keyword. There are a few Hexo plugins that have approached the subject, but it was not really satisfactory and performant. So I relied on the worlds biggest search engine: Google. A search button, which drives out a small input field and with the pressing of the ENTER key the form was sent via GET to //google.com/search. The procedure was simple, but also burdened with the fact that I always expose my users to Google. At least until now … :) Bryce Wray set me on a new path with his post Sweeter searches with Pagefind, in which he talks about his experience with the still fairly fresh tool Pagefind… Pagefind is a fully static search library that aims to perform well on large sites, while using as little of your users’ bandwidth as possible, and without hosting any infrastructure … --- Liam Bigelow @ pagefind.app Pardon me? A full text search for SSG’s running completely in the browser? It sounded so great that I had to try it right away. And what can I say … it not only works fantastically well, but is also extremely easy to implement. Of course, you have to consider a few things, especially with regard to the SSG Hexo I use, but I didn’t find any big hurdles, also because the tool is so well documented. Let’s see what my implementation looks like…","keywords":"beginning blog wanted provide kind full text search order give users ability find stuff keyword hexo plugins approached subject satisfactory performant relied worlds biggest engine google button drives small input field pressing enter key form //googlecom/search procedure simple burdened fact expose … bryce wray set path post sweeter searches pagefind talks experience fairly fresh tool pagefind… fully static library aims perform large sites bandwidth hosting infrastructure --- liam bigelow pagefindapp pardon ssgs running completely browser sounded great works fantastically extremely easy implement things regard ssg didnt big hurdles documented lets implementation like…","text":"From the beginning of this blog I wanted to provide some kind of full text search in order to give my users the ability to find stuff by keyword. There are a few Hexo plugins that have approached the subject, but it was not really satisfactory and performant. So I relied on the worlds biggest search engine: Google. A search button, which drives out a small input field and with the pressing of the ENTER key the form was sent via GET to //google.com/search. The procedure was simple, but also burdened with the fact that I always expose my users to Google. At least until now … :) Bryce Wray set me on a new path with his post Sweeter searches with Pagefind, in which he talks about his experience with the still fairly fresh tool Pagefind… Pagefind is a fully static search library that aims to perform well on large sites, while using as little of your users’ bandwidth as possible, and without hosting any infrastructure … --- Liam Bigelow @ pagefind.app Pardon me? A full text search for SSG’s running completely in the browser? It sounded so great that I had to try it right away. And what can I say … it not only works fantastically well, but is also extremely easy to implement. Of course, you have to consider a few things, especially with regard to the SSG Hexo I use, but I didn’t find any big hurdles, also because the tool is so well documented. Let’s see what my implementation looks like… The Tool in BriefPagefind it is a Node.js tool and is started via the Node Package Runner (npx) and runs against the static files already created during the build. It indexes all the desired pages or even parts of the pages and creates meta and index files for them in a special build folder, which can be retrieved later via JavaScript. To make things a bit more user-friendly, Pagefind also directly generates the necessary JavaScript and CSS files for a UI. … but Liam can explain better how it works: Implementation in HexoFirst of all I decided to store all necessary parameters in a supported config file in the root of my blog project. pagefind.yml123456source: docsbundle_dir: pagefindexclude_selectors: - ".note-list" - ".anything-list" - ".article-related" source defines the relative folder where all static files are created during the build and which should now be indexed. bundle_dir overrides the default storage folder called _pagefind, which is created in the build folder for the search files. This is necessary because my blog is built and hosted on Github Pages and the responsible GitHub Action goes over folders with a starting underscore on deployment. More info on that here and here. exclude_selectors is a list of all those page elements whose content should NOT be indexed, but more about that later. With another setting called glob it is possible to tell Pagefind which files to index, but this currently has its pitfalls when trying to exclude some. Liam already has this on the screen for one of the next versions. Limiting Indexing ContentA post on a web page never stands alone, but is surrounded by other elements such as navigation, further links, etc. However, these addional elements should not end up in the index. Pagefind skips some of them like nav, form or script automatically, but there always remain some that should be excluded by hand. Best option to narrow down the indexable content is the use of the attribute data-pagefind-body. Instead of excluding something, tell Pagefind what to include. However, this approach makes it easier, but also has consequences: If data-pagefind-body is found anywhere on your site, any pages without this attribute will be removed from your index. --- pagefind.app In my case, I had a few places in my templates that I needed to add the attribute to: Type File Element Post _partial/article.ejs article Notes note.ejs article Page page.ejs .page-content Dynamic Page [name].ejs .page-content Anything Page _partial/anything-page-item.ejs .anything-content All elements inside of the elements attributed like that, I had to exclude via the setting exclude_selectors in the config (see above). Specifying Meta InformationIt was important to me to show the date of a post in the search result, because nothing is as inaccurate as a post that is many years old. With Pagefind you select the HTML element in the templates in which the meta value is located and attribute it with data-pagefind-meta, for example: date.ejs1234<time class="published dt-published" itemprop="datePublished" data-pagefind-meta="date"> <%= date(page.date, 'DD MMM YYYY') %></time> As title for the search hit Pagefind searches for H1 tags and takes the value of the last tag it finds. If you are not sure that there is always only one H1 tag on the page (and for me it is), then you better specify which tag it should take: title.ejs123<h1 class="<%= class_name %>" itemprop="name" data-pagefind-meta="title"> <%= post.title %></h1> Thus, on specifying meta data you can refer not only to the content of a tag, but also to other attributes. Here’s the example for my special Hexo implementation for header images: photograph.ejs1234<img id="header-photo" data-pagefind-meta="image[src], image_alt[alt]" src="/photos/normal/<%= page.photograph.file %>" alt="<%= page.photograph.name%>" width="0" height="0"/> In case there is simply no element that contains the meta value, you can also specify it within the attribute: article.ejs12345<article id="note-<%= page.slug %>" itemprop="blogPost" data-pagefind-body data-pagefind-meta="type:Note"> ...</article> Adding a Search PagePagefind includes not only everything to get search results from the created index by JavaScript, but also the complete UI for a search page, which means that you can build the complete search into your site yourself, or simply take a pre-built UI and visually customize it if necessary. I did the latter. The following code is the basic structure of the search page as suggested by Pagefind: 123456789<link href="/pagefind/pagefind-ui.css" rel="stylesheet"><script src="/pagefind/pagefind-ui.js" type="text/javascript"></script><div id="search"></div><script> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({ element: "#search" }); });</script> To accommodate this in Hexo, it is advisable to use your own template and generate the page with an appropriate generator. A standard PAGE in Markdown format is only conditionally suitable for this, because links and scripts are needed. I described how to implement such a generator that renders descriptive Markdown in addition to the EJS template in my post Pattern for dynamic Hexo pages, and I’ve taken that approach here as well. For simplicity, I won’t list the full code here, but link to my blog’s GitHub repo: File Description /source/_dynamic/search.md Markdown file with Frontmatter data and introduction text /themes/landscape/layout/search.ejs Layout template for search page /themes/landscape/script/generator-dynamic-search.js Hexo Generator for creating the page during build /themes/landscape/script/source/css/_pagefind.styl Customized CSS Variables and style overrides For the visual customization of the user interface Pagefind provides some CSS variables in the automatically generated CSS file. These help a bit to customize the UI to your own ideas, but I decided to override some of the styles in a seperate file called _pagefind.styl, which will be bundled via @import "_pagefind" in the main styles.styl. Since the main bundled CSS file is loaded in the HEAD before the _pagefind.css somewhere in the page, for simplicity I first made sure to pull the overrides with !important. This is not yet pretty and I will have to revise this later on. Running Build and PagefindThus prepared, the rest is a piece of cake. Pagefind does not need to be installed, because if you call the npm package via npx, the latest version will be downloaded and executed automatically. You just have to make sure that the hexo build has run before. The best way is to run the following command: hexo clean && hexo generate &&npx pagefind This my result in the console: 123456789101112131415161718192021222324Running Pagefind v0.10.7 (Extended)Running from: "...\\\\kiko.io"Source: "docs"Bundle Directory: "pagefind"[Walking source directory]Found 319 files matching **/*.{html}[Parsing files]Found a data-pagefind-body element on the site.↳ Ignoring pages without this tag.[Reading languages]Discovered 1 language: en[Building search indexes]Total: Indexed 1 language Indexed 124 pages Indexed 7220 words Indexed 0 filters Indexed 0 sortsFinished in 5.924 seconds Mount in GitHub ActionSince I am hosting this blog on GitHub Pages and the complete build and deployment is done by a GitHub Action, I added a step to the hexo-build job in the workflow file so that after the build Pagefind indexes the result: hexo-build.yml12345678910jobs: hexo-build: runs-on: ubuntu-latest steps: ... - name: Build run: npm run generate - name: Run Pagefind run: npm_config_yes=true npx pagefind ... Thankfully, in his article on Pagefind, Bryce also put me on the track of how to prevent a possible security prompt caused by npx from blocking Pagefind to run … npm_config_yes=true. The ResultThe finished solution is really amazing. As soon as you start typing, the included Pagefind JavaScript updates the results list … and it’s sooo fast. Really an exciting tool. Thanks to Liam and CloudCannon. I hope that my explanation has inspired you to try it out for yourself on your Hexo driven blog or website. If you need some help or advice, drop me a line… More Info CloudCannon: PagefindLiam Bigelow: Introducing Pagefind: Static Low-bandwidth Search at ScaleBryce Wray: Pagefind is quite a find for site searchBryce Wray: Sweeter searches with PagefindNicolas Deville: Pagefind","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"},{"name":"Search","slug":"Search","permalink":"https://kiko.io/tags/Search/"}]},{"title":"Favourite Pens of 2022","subtitle":null,"date":"2023-01-14","updated":"2023-01-14","path":"post/Favourite-Pens-of-2022/","permalink":"https://kiko.io/post/Favourite-Pens-of-2022/","excerpt":"Every year CodePen publishes a list of the 100 most “liked” pens on their site: The Most Hearted of 2022. It’s always exciting to scroll through the list and marvel at the incredibly good work of CSS artists. My favorite in terms of CSS coding art this year is the work A moment of pure CSS by Ben Evans. Absolutely amazing what he does with pure CSS: From all these works you can take a lot of know-how for yourself, but many of these pens have no practical use at first, i.e. you can’t really use them directly on your own website. They are art. Some of them impressed me not only because of their creativity, but I saved them on my own Trello list to try them out in one of the next projects. Partly they are clever approaches regarding usability, partly more or less standard functions were implemented in a visually impressive way. Some of them need some JS to work, sime of them not. Let yourself be inspired …","keywords":"year codepen publishes list pens site hearted exciting scroll marvel incredibly good work css artists favorite terms coding art moment pure ben evans absolutely amazing works lot know-how practical directly website impressed creativity saved trello projects partly clever approaches usability standard functions implemented visually impressive js sime inspired …","text":"Every year CodePen publishes a list of the 100 most “liked” pens on their site: The Most Hearted of 2022. It’s always exciting to scroll through the list and marvel at the incredibly good work of CSS artists. My favorite in terms of CSS coding art this year is the work A moment of pure CSS by Ben Evans. Absolutely amazing what he does with pure CSS: From all these works you can take a lot of know-how for yourself, but many of these pens have no practical use at first, i.e. you can’t really use them directly on your own website. They are art. Some of them impressed me not only because of their creativity, but I saved them on my own Trello list to try them out in one of the next projects. Partly they are clever approaches regarding usability, partly more or less standard functions were implemented in a visually impressive way. Some of them need some JS to work, sime of them not. Let yourself be inspired … 1. iOS Notifications by Yoav Kadosh 2. Lotsa Notifications by Jon Kantner 3. Animated Star Rating by Jon Kantner 4. Changing Face Rating by Jon Kantner 5. CSS Minimal Dark Mode Toggle Button by Greg Vissing 6. Menu micro-interaction with CSS by Mert Cukuren 7. Animated BottomBar Experiment by Chris Bautista 8. Progress Button by Taylon, Chan 9. Gradient background with waves by Bárbara Rodríguez 10. Cascading CSS Text Effects by Jhey","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"UI","slug":"UI","permalink":"https://kiko.io/tags/UI/"}]},{"title":"Discoveries #22 - Tips/Tricks","subtitle":null,"series":"Discoveries","date":"2023-01-06","updated":"2023-01-06","path":"post/Discoveries-22-Tips-Tricks/","permalink":"https://kiko.io/post/Discoveries-22-Tips-Tricks/","excerpt":"As someone said the other day? “January is the Monday of the year”. How true. After programming myself a new workflow for creating my discoveries (see Generate Content from Trello) at the end of last year, I wanted to try it out again right away and give you a list of tips and tricks for starting 2023. 6 steps to get verified on Mastodon with encrypted keysGenerate a Pull Request of Static Content With a Simple HTML FormMy Wonderful HTML Email Workflow, using MJML and MDX for responsive emailsHow to View Build Logs for GitHub PagesEnabling IntelliSense for GitHub Actions workflows in VS Code9 JavaScript Console Tips That Will Improve Your Debugging SkillsFun with console.log()Load hierarchical data from MSSQL with recursive common table expressionsAn HTML-first Mental ModelProject Documentation with Hexo Static Site Generator","keywords":"day january monday year true programming workflow creating discoveries generate content trello end wanted give list tips tricks starting steps verified mastodon encrypted keysgenerate pull request static simple html formmy wonderful email mjml mdx responsive emailshow view build logs github pagesenabling intellisense actions workflows code9 javascript console improve debugging skillsfun consolelogload hierarchical data mssql recursive common table expressionsan html-first mental modelproject documentation hexo site generator","text":"As someone said the other day? “January is the Monday of the year”. How true. After programming myself a new workflow for creating my discoveries (see Generate Content from Trello) at the end of last year, I wanted to try it out again right away and give you a list of tips and tricks for starting 2023. 6 steps to get verified on Mastodon with encrypted keysGenerate a Pull Request of Static Content With a Simple HTML FormMy Wonderful HTML Email Workflow, using MJML and MDX for responsive emailsHow to View Build Logs for GitHub PagesEnabling IntelliSense for GitHub Actions workflows in VS Code9 JavaScript Console Tips That Will Improve Your Debugging SkillsFun with console.log()Load hierarchical data from MSSQL with recursive common table expressionsAn HTML-first Mental ModelProject Documentation with Hexo Static Site Generator 6 steps to get verified on Mastodon with encrypted keys by Seth Kenlon https://opensource.com/article/22/12/verified-mastodon-pgp-keyoxide To verify that you control your Mastodon account, the easiest way is to add a verification link in your profile, which points to your blog/website and where Mastodon find a link attributed with 'rel=me'. For advanced verification you can use the power of shared encrypted keys, which Mastodon can link to thanks to the open source project Keyoxide … and Seth shows how to get it. Generate a Pull Request of Static Content With a Simple HTML Form by Hilman Ramadhan https://css-tricks.com/generate-a-pull-request-of-static-content-with-a-simple-html-form/ Hosting your static files blog/site/whatever on GitHub and wan't others to contribute? Hilman has an idea to achieve this via a standard form, that creates a Pull Request! My Wonderful HTML Email Workflow, using MJML and MDX for responsive emails by Josh Comeau https://www.joshwcomeau.com/react/wonderful-emails-with-mjml-and-mdx/ Writing HTML E-Mails can be challenging, because you can't use all the modern stuff. For a good reason the technique building mails has stuck in the 90s. Josh's tutorial is about using the framework MJML (Mailjet Markup Language), which offers an abstraction layer over raw HTML. How to View Build Logs for GitHub Pages by Rizèl Scarlett https://dev.to/github/visualize-github-pages-build-logs-1mc1 GitHub Pages are build with Jekyll and as the deployments runs with GitHub Actions it's easy to view the build details. But more interesting in Rizèl's article is as he describes a fully custom deployment without Jekyll. Enabling IntelliSense for GitHub Actions workflows in VS Code by Gérald Barré https://www.meziantou.net/enabling-intellisense-for-github-actions-workflows-in-vs-code.htm There are som VS Code Plugins out there, which supports Intellisense while writing workflow YAML files for configuring GitHub Actions. Gérald has a tip how to achieve that manually. 9 JavaScript Console Tips That Will Improve Your Debugging Skills by Sunil Sandhu https://blog.bitsrc.io/9-javascript-console-tips-that-will-improve-your-debugging-skills-1899e37469d5 The console is more powerful than you might think. Sunil talks here about the possibilities to debug a bit better and more efficient. I have to use 'time' more often… Fun with console.log() by Alicia Sykes https://dev.to/lissy93/fun-with-consolelog-3i59 In addition to Sunils tips above, Alicia summarizes it here and has some more tips for efficient debugging in the browser. Load hierarchical data from MSSQL with recursive common table expressions by Robert Muehsig https://blog.codeinside.eu/2019/03/31/load-hierarchical-data-from-mssql-with-recursive-common-table-expressions/ Designing a hierachie inside MS SQL can be painfull, but at least there is a way to load this data in a fast way, as Robert shows. An HTML-first Mental Model by Noam Rosenthal https://calendar.perfplanet.com/2022/an-html-first-mental-model/ Noam, from Google Chrome's speed metrics team, writes about his experiences on developing a showcase movies app using different frameworks regarding speed and performance in the browser and why we always keep good old HTML in mind. Project Documentation with Hexo Static Site Generator by Bruno Mota https://www.sitepoint.com/project-documentation-hexo/ Bruno Mota looks at how you can create project documentation using Hexo, the static site generator built on Node.js, and deploy easily to GitHub Pages. Some stuff to learn there for me, who runs this blog nearly the same way…","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Generate Content from Trello","subtitle":"Use Trello board as bookmark data source for generating Hexo content","series":"A New Blog","date":"2022-12-29","updated":"2022-12-29","path":"post/Generate-Content-from-Trello/","permalink":"https://kiko.io/post/Generate-Content-from-Trello/","excerpt":"I’m collecting/bookmarking links to interesting website post on a public Trello board and aggregating them from time to time in a special post series called Discoveries, where I present 10 of them in one post on a specific topic. Writing these summaries is currently still a bit time-consuming, because in addition to the link, the name of the author and a small description, I would also like to display a screenshot of the page in each case … and it is still a completely manual process. After selecting the 10 links I want to talk about, I first create a new post for my Hexo blog and then process the links as follows: Creating, resizing and saving the screenshot Creating a new section in the post Setting a key for the post based on the title Setting the title Setting the author Setting the screenshot file I do use two tag plugins (tag-anchorlist and tag-discovery) for this in the post draft, but despite that, it’s quite time-consuming and always the same procedure that can be wonderfully automated… and here I’d like to write about how I approached this task, while I’m working on it.","keywords":"im collecting˻ookmarking links interesting website post public trello board aggregating time special series called discoveries present specific topic writing summaries bit time-consuming addition link author small description display screenshot page case … completely manual process selecting talk create hexo blog creating resizing saving section setting key based title file tag plugins tag-anchorlist tag-discovery draft procedure wonderfully automated… id write approached task working","text":"I’m collecting/bookmarking links to interesting website post on a public Trello board and aggregating them from time to time in a special post series called Discoveries, where I present 10 of them in one post on a specific topic. Writing these summaries is currently still a bit time-consuming, because in addition to the link, the name of the author and a small description, I would also like to display a screenshot of the page in each case … and it is still a completely manual process. After selecting the 10 links I want to talk about, I first create a new post for my Hexo blog and then process the links as follows: Creating, resizing and saving the screenshot Creating a new section in the post Setting a key for the post based on the title Setting the title Setting the author Setting the screenshot file I do use two tag plugins (tag-anchorlist and tag-discovery) for this in the post draft, but despite that, it’s quite time-consuming and always the same procedure that can be wonderfully automated… and here I’d like to write about how I approached this task, while I’m working on it. The Trello ListHow easy it is to save a link as a card in a Trello board via Chrome, I described recently in my post Add website to Trello card the better way. As this works also in Chrome on Android, I store interesting links on the go mostly. In order to get all information I need later on, I have extended my collections board with a custom field called ‘Author’. For adding the screenshot to the card as an attachment, I use actually the build-in feature of Android 6. As I have a long, long list if incoming links, I sort them by topic into an appropriate list, for example, “Discoveries: JS Libraries” and this lists should then be automatically turned into new blog posts as I want to publish one. The DataNow, to get the raw data of a Trello list, I could use to the Atlassian API, but that’s not even necessary, because each board can be accessed machine-readable per se via adding .JSON to the board url. Prerequisite is that the board is set to PUBLIC. The URL is structured according to the following scheme: https://trello.com/b/<WORKPLACE-ID>/<BOARD-NAME>.json The URL accepts following parameters (as far as I found out), to filter out some not needed content: fields (string) lists (string) list_fields (string) cards (string) card_fields (string) card_attachments (bool) card_attachment_fields (string) customFields (bool) card_customFieldItems (bool) members (bool) member_fields (string) organization (bool) checklists (string) checklist_fields (string) labels (string) actions (string) action_fields (string) actions_limit (number) All boolean parameters can assume true or false and the string parameters either all, none or (some) a comma-separated value list of fields to show. For example: https://trello.com/b/o2tmzJAw/test.json?fields=none&lists=all&list_fields=name&cards=all&card_fields=desc,idList,name … shows up like this: Trello JSON (shortened)1234567891011121314151617181920212223{ "id": "63a871b59e3b200022455381", "cards": [ { ... }, { "id": "63a8586e7de45d0fc54d0d39", "desc": "Kate shows us how to create a tree view as collapsible list, created using only html and css, without the need for JavaScript", "idList": "63a8585dc0c10c020ca9ea03", "name": "Tree views in CSS" }, { ... } ], "lists": [ { "id": "63a997ab23684f02303a0525", "name": "Discoveries INBOX" }, { "id": "63a8585dc0c10c020ca9ea03", "name": "Discoveries: CSS" } ]} Important to now here is, that most of the data are NOT hierarchially structured, like Board > List > Cards, but in parallel. You have to pick the id of a list to filter the cards array by it. The same with custom fields inside a card: it holds a reference to the custom field list only. For the attachments of a card, Trello distinguishes between URL’s and files. The attribute bytes is null for URLs and the URL itself is in name. Files, on the other hand, have bytes greater than 0 and a specific mimeType, while images additionally have up to seven different previews in the widths 70, 150, 250, 300, 600 and original. Very handy for my case, since I always scale down my screenshots to 600 pixels. https://trello.com/b/o2tmzJAw/test.json?fields=none&lists=all&list_fields=name&cards=all&card_fields=desc,idList,name&card_attachments=true&card_attachment_fields=bytes,mimeType,name,previews&customFields=true&card_customFieldItems=true ... card item in Trello JSON (shortened)123456789101112131415161718192021222324252627282930313233{ "id": "63a8586e7de45d0fc54d0d39", "desc": "Kate shows us how to create a tree view as collapsible list, created using only html and css, without the need for JavaScript", "idList": "63a8585dc0c10c020ca9ea03", "name": "Tree views in CSS", "attachments": [ { "bytes": null, "mimeType": "", "name": "https://iamkate.com/code/tree-views/", "previews": [], "id": "63a8586e7de45d0fc54d0d5b" }, { "bytes": 225271, "mimeType": "image/png", "name": "Screenshot_20221225-121537.png", "previews": [ { ... }, { "_id": "63a8586e7de45d0fc54d0d58", "id": "63a8586e7de45d0fc54d0d58", "scaled": true, "url": "https://trello.com/1/cards/63a8586e7de45d0fc54d0d39/attachments/63a8586e7de45d0fc54d0d52/previews/63a8586e7de45d0fc54d0d58/download/Screenshot_20221225-121537.png", "bytes": 111574, "height": 499, "width": 600 }, { ... }, ], "id": "63a8586e7de45d0fc54d0d52" }} The GeneratorFirst of all: This generator has NOTHING to do with Hexo’s build-in generators. It’s just a Node script, which produces MD files that later on will be processed by Hexo into posts! What should he do: Download Board JSON from Trello Iterate through lists to find the one to process Iterate through cards to find all referencing the chosen list Create new POST object to store all needed information Process all found cards… Create new ITEM object to store all needed information Store TITLE, generated KEY out of TITLE, DESCRIPTION in ITEM Resolve AUTHOR from customfields for ITEM Iterate through card attachments Store URL in ITEM, when its a link Generate IMAGENAME out of KEY and store in ITEM, when its an image Create new POST.FOLDER for the images Download image from attachment URL into POST.FOLDER as IMAGENAME Add ITEM to POST.ITEMS Get photograph for new post (kiko.io special, see Automatic Header Images in Hexo) Store PHOTOGRAPH information in POST Generate new post via Handlebars template Store new post The goal is that I only need to write an introduction and adjust a few frontmatter variables before generating and publishing the post. SettingsFirst task is to save the possible settings in Hexo’s default configuration file: 123456789101112131415161718192021222324# Trello Discoveries Generator Scriptdiscoveries: board: url: https://trello.com/b/D6zIhLus/collections.json parameters: - key: fields value: name - key: lists value: all - key: list_fields value: name - key: cards value: all - key: card_fields value: closed,desc,idList,name - key: card_attachments value: true - key: card_attachment_fields value: bytes,mimeType,name,previews - key: customFields value: true - key: card_customFieldItems value: true template: discoveries.handlebars TemplateNext step is creating a Handlebars template out of my scaffold file I used so far: 123456789101112131415161718192021222324252627282930313233343536373839---slug: {{{key}}}title: '{{{title}}}'subtitle:date: {{{date}}}photograph: file: {{{photograph.file}}} name: {{{photograph.name}}} link: {{{photograph.link}}} socialmedia: /static/images/social-media/{{{key}}}.pngseries: Discoveriescategories: - Misctags: - Collectionrelated: - Discoveries-xx - Discoveries-yy - Discoveries-zzsyndication: - host: Mastodon url: ---INTRODUCTION...{% anchorlist {{#each items}} "{{{title}}}|{{{key}}}"{{/each}}%}<!-- more -->{{#each items}}{% discovery "{{{title}}}" "{{{author}}}" "{{{url}}}" {{{../key}}} {{{imageName}}} %}{{{description}}}{% enddiscovery %}{{/each}} Generator ScriptNext, the generator itself, which lives in /lib/scripts/. It is implemented as a class with CommonJS and takes two parameters in the constructor for defining the number of the Discoveries post to create and the name of the Trello list, where the data for this should come from. It’s main function is generate, which starts the generation. Here’s the skeleton: discoveries-generator.js123456789101112131415161718192021222324252627282930313233343536const fs = require("fs");const yaml = require('js-yaml');const path = require("path");const axios = require("axios");const handlebars = require("handlebars");const { marked } = require('marked');class Generator { let _config; let _trelloUrl; let _templateFile; // Init new post object let _post = { board: null, list: null }; constructor(discoveryNo, listName) { // Init the generator and gather all necessary data for running it } generate() { // Run the generator } async downloadImage(url, item) { // Helper for downloading the images asynchronously } createPostFromTemplate() { // Helper for creating the post's MD file out of the template }} At this point I will refrain from reproducing the complete code here. Just follow this link … However, a few points in the implementation are important to consider: The downloads are performed by means of the promise based HTTP client axios. I can only recommend this thing… The image downloads are initially collected in a Promise list for subsequent execution, while iterating through the cards of the selected Trello list The script is actually really straight forward, but I have some improvements in mind, which will find their way into the code later on: A. If an image is missing, create a proper screenshot via Puppeteer B. Introduce a top most card(s) for the INTRODUCTION, a SUBTITLE and some additional TAGS, to avoid having to rework the new post before publishing C. Automated insertion of the RELATED posts, based on the last three Discoveries issues Run it …The easiest way to get the generator running, is to create a simple runner script: _run_discoveries-generator.cjs1234567const Generator = require("./discoveries-generator.cjs").Generator;const discoveryNo = process.argv[2].toString();const listName = process.argv[3].toString();const generator = new Generator(discoveryNo, listName);generator.generate(); The execution in the console then is just a one-liner: node "./lib/_run_discoveries-generator.cjs" "<NUMBER>" "<TRELLO-LISTNAME>" It was fun to write this automation during Christmas. Also kept me from stuffing too many cookies inside me ;)","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Trello","slug":"Trello","permalink":"https://kiko.io/tags/Trello/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"}]},{"title":"Discoveries #21 - Sites & Pages","subtitle":null,"series":"Discoveries","date":"2022-12-28","updated":"2022-12-28","path":"post/Discoveries-21-Sites-Pages/","permalink":"https://kiko.io/post/Discoveries-21-Sites-Pages/","excerpt":"Websites always have a certain purpose and depending on what you want to do with it, there are pre-designed tools, that make it very easy to get started. In this issue of Discoveries I have collected a few such enablers. Also included are two hosting offers that address the hot topics of #Fediverse and #IndieWeb in times of #TwitterMigration. Webmention AnalyticsIndiekitMasto.hostStatic Timeline GeneratorGlossary Page TemplateMarkdocdocs.pageJPageGitHub OCTO: Flat DataBatNoter","keywords":"websites purpose depending pre-designed tools make easy started issue discoveries collected enablers included hosting offers address hot topics #fediverse #indieweb times #twittermigration webmention analyticsindiekitmastohoststatic timeline generatorglossary page templatemarkdocdocspagejpagegithub octo flat databatnoter","text":"Websites always have a certain purpose and depending on what you want to do with it, there are pre-designed tools, that make it very easy to get started. In this issue of Discoveries I have collected a few such enablers. Also included are two hosting offers that address the hot topics of #Fediverse and #IndieWeb in times of #TwitterMigration. Webmention AnalyticsIndiekitMasto.hostStatic Timeline GeneratorGlossary Page TemplateMarkdocdocs.pageJPageGitHub OCTO: Flat DataBatNoter Webmention Analytics by Max Böck https://mxb.dev/blog/webmention-analytics/ Your blog supports Webmentions? Then you should have a look on Max project, which I'm also contributing to. It collects all Webmention data of your post and gives you a nice analytics page. Indiekit by Paul Robert Lloyd https://getindiekit.com/ This Node-driven server is everything you need to start into the IndieWeb. Publish and share your own content, integrate SSG's like Hugo or Jekyll and file storage on GitHub, GitLab or FTP. It works with the Micropub protocol and has a plugin API for developing extensions. Masto.host by Hugo Gameiro https://masto.host/ Since a crazed billionaire has decided to make something 'different' out of Twitter, the Fediverse alternative Mastodon gains more and more attraction. #TwitterMigration. But it is not easy to host an instance by yourself. Hugo jumps in here and offers a fully managed Mastodon hosting service. Static Timeline Generator by Molly White https://github.com/molly/static-timeline-generator Some data are best presented by a timeline in order to show what has happened in descending order. Best example is actually twitterisgoinggreat.com, which lists all things happened since Elon Musk has taken over Twitter. This site is built with Mollys static timeline generator. Glossary Page Template by Hilverd Reker https://glossary.page/template/ Ever had to maintain a glossary? Can be very time consuming, first of all to present it in a structured, searchable way. This project from Hilverd, available on GitHub, is a single HTML page with a build-in editor. Markdoc by Stripe Dev's https://markdoc.io/ Markdoc is a Markdown-based system for creating custom documentation sites, build by the guys from Stripe on order to provide a documentation of their service to their customers. It is available at GitHub and a good starting point for your next documentation. You are documenting your stuff, right? docs.page by Invertase Dev's docs.page Another way to easily create documentation. This time with the focus on GitHub projects and from the developers of the British company Invertase It is as simple as the name suggests: create a /docs folder in the repository, put the configuration in a docs.json and start with the first MDX file… JPage by Pedro Isac https://pedro-isacss.github.io/jpage/ Some sites just need to have some slides to present the main purpose. Marketing stuff, photos, whatever. That's what Pedro built his JPage for. It supports slides in two axis and its configuration is just about the arrangement of standard HTML tags. Super simple and super effective. GitHub OCTO: Flat Data by Idan Gazit, Amelia Wattenberger, Matt Rothenberg, Irene Alvarado https://octo.github.com/projects/flat-data Flat is an experiment from the Developer Experience team in GitHub Next and gives you the possibility to fetch, aggregate and view data of many different types. It incorporates three different pieces: Flat Action to fetch data, Flat Editor and Flat Viewer. It's based on GitHub Actions and can be fully integrated in your repository. BatNoter by GitHub User vivekweb2013 https://github.com/batnoter/batnoter Batnoter is an open source, markdown-based, self-hosted note taking webapp, written with React, that uses your github repository to store markdown notes.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Hosting","slug":"Hosting","permalink":"https://kiko.io/tags/Hosting/"}]},{"title":"The State of the Blog","subtitle":"Recap & Workflow after 3 years with Hexo","series":"A New Blog","date":"2022-12-23","updated":"2022-12-23","path":"post/The-State-of-the-Blog/","permalink":"https://kiko.io/post/The-State-of-the-Blog/","excerpt":"I’ve started this blog in 2019 with this article primarily because I needed an area to record things learned for myself, with the side effect that others can benefit from it if they want. Why my choice fell on the static site generator Hexo, I no longer know, but I have now become accustomed (even to the shortcomings) and so far I have been able to implement all my ideas in it … and I had a few of them. In this post I would like to share a few experiences I have had with Hexo, regarding the main functionality and the things I’ve customized and describe my workflow behind the individual features of my blog. The latter is not as straightforward as I would like it to be, especially because I have different devices in use that require different approaches. The main purpose of this post is to simply write down for me, how things currently work on kiko.io and to have one or the other idea how to do it better while writing. Doing this publicly is in the hope that you might read this and have a terrific idea that I haven’t come up with yet and leave a comment or webmention … :)","keywords":"ive started blog article primarily needed area record things learned side effect benefit choice fell static site generator hexo longer accustomed shortcomings implement ideas … post share experiences main functionality customized describe workflow individual features straightforward devices require approaches purpose simply write work kikoio idea writing publicly hope read terrific havent leave comment webmention","text":"I’ve started this blog in 2019 with this article primarily because I needed an area to record things learned for myself, with the side effect that others can benefit from it if they want. Why my choice fell on the static site generator Hexo, I no longer know, but I have now become accustomed (even to the shortcomings) and so far I have been able to implement all my ideas in it … and I had a few of them. In this post I would like to share a few experiences I have had with Hexo, regarding the main functionality and the things I’ve customized and describe my workflow behind the individual features of my blog. The latter is not as straightforward as I would like it to be, especially because I have different devices in use that require different approaches. The main purpose of this post is to simply write down for me, how things currently work on kiko.io and to have one or the other idea how to do it better while writing. Doing this publicly is in the hope that you might read this and have a terrific idea that I haven’t come up with yet and leave a comment or webmention … :) Hexo …I don’t have experience with other SSGs like Hugo or others, so I can’t make comparisons, but I can share a few things about Hexo that have kept me engaged in my work with Hexo. I won’t go into the general functionality of Hexo here, but the ones that interest me most for my particular needs. A more or less good start into the tool is https://hexo.io with its DOCS and API. One of the first commands you get to know is generate, which generates the static HTML pages from the MD files stored in the source/_posts folder. You can add your own generators, which can generate any other custom pages, to this process and put them under themes/<your-theme/scripts for automatic execution. Every generator has a locals argument, which holds all site variables, including a list of all posts. Unfortunately, the documentation at hexo.io/api is rather poor and so only after a while I realized, that a custom generator is called relatively late in the process, when the MD files have long since been preprocessed. So you can process any MD files in a generator, but you have to do without the automatic processing of the powerful Tag Plugins or syntax highlighting, because this takes place in an upstream processor, which deals with boxes. I don’t understand Hexo’s concept at this point. For me it is not particularly catchy and straightforward. Unfortunately a few help-, discussion- and example pages are written in Chinese, since the creator Tommy Chen is from Taiwan, as I guess, but my Chinese really sucks… ;) The best thing about Hexo in general is, that you can get your first results quickly after the basic installation, so you are able to concentrate on writing and prettying things up for a while. Only when the requirements increase is it necessary to take a closer look at the insufficiently documented substructure. A challenge even for experienced web developers, but unfortunately absolutely impossible for beginners… PostsHexo is based on a structure of posts and pages, where posts can be aggregated in various ways, whether in the time-based archive or via tags and categories. Posts has to have a specific structure, regarding the Frontmatter in the MD file and teh storage of attached images. Hexo offers the user a separate command for this: hexo new, which works with template files in the folder scaffolds. For the presentation of my posts, I decided to incorporate my passion of photography by assigning a unique header image to each post (and most pages). How I implemented this, I have described in the article Automatic Header Images in Hexo. The main point is, when I create a new post, I automatically and randomly pick a photo (in the three media-dependent versions normal, tablet and mobile) from a pool, copy the images to the appropriate delivery folder and bind it to the new post via frontmatter. For this I’ve written the Hexo event hook on-new-get-photo-for-post.js, which runs on the command hexo new post "<title-of-new-post>". Works great, except for the fact that I’m often on the go and using a Node.JS-powered command towards an SSG project hosted on GitHub definitely doesn’t work on an Android smartphone. So, how do I get it to create a new post remotely? First part of the answer, regarding access to all project files, was to store the project in a folder that is synchronized with one of the usual suspects: Dropbox, OneDrive, GoogleDrive. With this, I have access to the entire project at all times on all devices that can sync the files. In the case of Android with the help of one of the Autosync apps from metactrl. The second part of the answer, regarding execution of the new post command, was a bit more difficult, but part of my solution was to use my little home server, where the project also syncs and which is always switched on. Problem here: This tiny box is not publicly available on the web and never will be. Therefore the only communication channel I had, was file synchronization and the solution was to use a simple text file to hold the commands I want to be executed. For this purpose, I have written a small command line tool that runs and processes a modified command file when it is received. For file monitoring I have SyncBackPro in use, which has the incredibly useful feature of being able to execute commands before and after a synchronization takes place, in this case my tool, which I have named HexoCommander and whose source code is available on GitHub. Before you think “What the hell is he doing? That’s what [fill in the Linux tool of your choice] is for“ … I’m more of a Windows guy. Linux means something to me, but I’ve never worked with it and never had to … and yessss, there is a career in IT also with Windows ;) The advantage of synchronizing the whole GitHub project via cloud storage service is that I also host kiko.io on Github Pages and that a commit to the repository on GitHub automatically triggers a deployment, which I don’t always want right away. So, because of my changes to the post process, I don’t use Hexo’s built-in drafts, but write on a new post until I’m done and then release by committing to GitHub. is there an easier way? I admit that on an Android smartphone, it’s a bit fiddly to write a special command to a text file and then wait 2 or 3 minutes for the file to sync, the new line to be read and executed, and the new post files to sync back to the smartphone, but it works. But I would rather have a smartphone capable interface, where I can enter my data and the whole thing runs at the push of a button. Currently I have no idea how to achieve this in a static site environment. Do you have one? PhotosAs I speak from photos … to provide header images for my new posts (or pages), I have prepared a pool of photos that I display on a special filterable page. This page is not one that comes with Hexo, but is based on a dynamic page generator I’ve created for this purpose. I wrote about it in 2021: Pattern for dynamic Hexo pages. But how do I fill this pool with new photos, especially because I need three variants of a new photo for the standard device classes Desktop (normal), Tablet and Mobile? I have a very specific workflow in Lightroom to edit my photos and convert them from RAW to JPEG while exporting, to add them to my 500px collection for example. The destination of these export is again my cloud storage. From time to time I pick some images from these export folders that I want to use on the blog. I do this mostly on my Android smartphone in a quiet minute and these are the steps and Android apps I need, to turn a high resolution 10MB photo into one that can be used on the web: Solid Explorer: Creating a new folder in the projects pool folder with the name of the image. Solid Explorer: Copy the original photo into the new pool folder. Solid Explorer: Create meta.txt to hold the name and the 500px Url of the photo in the new pool folder. ImageSize: Create normal (1280px), tablet (768px) and mobile (480px) versions of the photo as web images. Solid Explorer: Copy web images to new pool folder and delete original. I have a similar workflow working on my Windows machine, but both workflows are manual ones. is there an easier way? Fiddling around with images on the smartphone like this almost has a medidative effect on me, but it’s not really effective. Better would be to throw the original image into a folder and let Hexo do the work of resizing by utilizing Sharp and imagemin. Series & ProjectsRelatively soon it was clear to me, that I want to aggregate posts to series and I didn’t want to use the integrated categories or tags for it. It should be working with a new frontmatter attribute called series with the value of a slug of a referencing MD file, which provides some text to describe the series. Projects is basically a index like series, but merely works with a different Frontmatter attribute. The whole thing was made possible by Levi Wheatcroft‘s work on his plugin hexo-index-anything. Unfortunately Levi stopped working on it a long time ago and so I forked it under the new name hexo-generator-anything. The generator is controlled by a section in Hexo’s configuration file, which defines the EJS layout file to render the main index page respectively the index pages of each series, beside a mapping list for the appropriate Frontmatter variable and the output path: _config.yaml12345678anything: layout_index: anything-index layout_posts: anything-posts index_mappings: - variable: series path: series - variable: project path: projects Tag Plugins and syntax highlighting for code blocks in the reference MD files are not resolved actually, because it is a generator only (see first section ‘Hexo…’). Tiny-ToolsSince this blog is primarily a memory aid for me and my future self (how often do I have stumbled across my own posts during web search), it was logical to place a special bookmark collection here, because it happened to me more often that I found and used an online tool and some time later wonder what the thing was called when I have the same requirement again. That’s why I started to collect these URL’s in a public Trello board (see Add website to Trello card the better way) and provide them with keywords and a screenshot. But Trello is a Kanban tool and not designed for such special requirements as the display of bookmarks and so I use it only as a data source for my page TINY-TOOLS. Generating a static page out of Trello cards was again a job for my dynamic pages, I had introduced for the photos page. It takes advantage of the fact that any Trello board can be retrieved as machine-readable data by extending the url with .json. The Dynamic Trello Generator is really straight forward. With this script it is only a matter of configuration to generate a page from a given list of a board (URL ist shortended): 1234567trello: boards: - name: Collections url: https://trello.com/.../collections.json?fields=all&cards=all&card_fields=all&card_attachments=true&lists=all&list_fields=all pages: - name: tiny-tools list: TinyTools is there an easier way? Can’t imagine one. Do You? NotesThe Notes section is the latest and with it I want to follow the POSSE principle, i.e. “Publish (on your) Own Site, Syndicate Elsewhere”, which is a vital part of the IndieWeb movement. So when something comes to mind that I want to post on Mastodon for example, I want it to end up in a new Note MD file that is then posted, preferably automatically. For me, notes differ from normal posts. From the beginning I wanted to present them chronologically connected on one page per year, like a diary, and not to mix them with the articles (posts). So a standard treatment as a Hexo post was out of the question and I had to come up with something of my own. Another reason not to work with the standard structures of Hexo was, that I did not want to create a subfolder with the name of the post for images as usual in Hexo, but one folder for all images of a year. Since I am familiar with Hexo generators, I built a new one called generator-notes that iterates through the entire source/_notes folder and generates an index file for all the notes in a year, as well as the notes file itself, and copies the images to the source path. This approach also freed me from using my HexoCommander, because there is no need of executing a new xxx command. I can now create new Notes from the smartphone quite easily: Autosync App: Synchronization of the source/notes folder Markor: Edit or create new Note MD file via snippet. GitHub PWA: Upload MD file to the appropriate folder of the repository The upload will automatically trigger the build and deployment to GitHub. Done. is there an easier way? Using a generator directly means, that I can’t use tag plugins in my Notes and code blocks won’t be prettied up, as I mentioned earlier. Also the Notes are not included in the Hexo Locals and are therefore not available later on for RSS feeds or other stuff to create. So I will probably extend my approach with a custom Hexo processor, that will allow me to do this. DeploymentAs I mentioned earlier, kiko.io is hosted at GitHub Pages. Due to a bug in Hexo regarding the treatment of non markdown files in post folders (Fix #1490), I had to process all files locally and transfer the output folder to the repository as well. I would have liked to use a GitHub action for the entire deployment, including the generation of the output files, but for a long time there was no NPM package with a working Hexo version that would have allowed this. I recently checked again to see if the current Hexo release 6.0.3. had fixed my bug and the subsequent bugs …. and yes. With one or two workarounds, my Hexo installation now runs completely on NPM packages, without any manual adjustments!So I could finally switch the deployment and automate it with its own build action. This enables me to write a text on the go, commit the file via github.com and the build and deployment starts. Woohoo … \\o/ is there an easier way? Nope. The only drawback I see is that this approach always generates the entire site and not just the newly added posts, as is the case locally. It’s a bit of a waste of time and computing resources. SyndicationAs I’m a fan of the Indieweb, I started early with Webmentions on my blog. The idea behind it is to mention an article from blog A in a post on Blog B or social media like Twitter or Mastodon and blog A gets an automated message like “Hey, I’ve mentioned you here“. The operator of Blog A can now also automatically display this message under the article mentioned. Receiving Webmentionskiko.io is a static site, therefore, there are no active components on the web server that could react to an incoming webmention. But that’s not a problem, because there are services like webmention.io that can serve as recipients and from where you can then pick up the messages. Some bloggers do this by fetching and inserting them into the articles while generating the static pages, but for me this can take too long. My approach is, to load the mentions from webmention.io via JavaScript dynamically into the page, as described in my post Hexo and the IndieWeb (Receiving Webmentions). is there an easier way? I don’t think so… Sending WebmentionsUnfortunately a I can’t integrate sending Webmentions for the mentioned Url’s in a particular (new) post into the generation process while building the static files for the blog, because the target Url’s not yet exist at this point of time. I have to wait until it is deployed and thus publicly available. So this step is currently still a manual one. My current solution is based on Remy Sharp’s NPM package @remy/webmention, which he himself also uses for his web service webmention.app. I integrate it into a Hexo console command called “console-webmention” which I call via hexo webmention. Without parameters, the last post is searched for outgoing url’s and their webmention endpoints respectively. See the project page for more details. is there an easier way? This works so far, but depending on a console application means once again having to use my HexoCommander and write commands to a text file when I’m on the road. Then I can also send the webmentions for a post directly manually via the Telegraph service. One possibility would be a cron job in a Github Action, but this would require creating a new container with all dependencies every time the build has just run and Github has deployed the created files, just to run a console command that might not even return any results since there is nothing to mention in the last post. Unsatisfactory. Another possibility would be to utilize my Atom feed by setting up a Zapier, IFTT or Make/Integromat task, which picks the last added entry and run the Telegraph API against it. Any ideas? Publish on MastodonI’m relatively new to Mastodon (Nov 22), but in the past I have syndicated some of my posts manually on Twitter. Now, that I’ve said goodbye to the greedy bird site, I’m thinking about how I’m going to automate publishing the notes in particular, but also at least an excerpt from a post, on Mastodon. What I do in the meantime? After the site is successfully deployed, I open up my Mastodon client and copy the complete content of a note or write an excerpt of a post manually into a new toot and append the permalink. After hitting SEND, I copy the newly created Mastodon post URL into a Frontmatter attribute called syndication of the note or post and commit the file to GitHub. Build and deployment runs again and the syndication link will be visible on kiko.io. is there an easier way? The Mastodon API is really simple and there are many examples written in Python, JS and other languages out there that are easy to adapt. But here I have the same problem as when sending webmentions: the target url is not yet available at the time of generating the page, when some code is running to do something. It must be a downstream task as well. I need some kind of service running on a server, where I only have a static site. Independent GitHub Action or Azure Function, triggered by a webhook after deployment is finished, which processes all notes and posts without the syndication Frontmatter? Or extending HexoCommander again, but how to trigger this buddy automatically, as he depends on file synchronization? SummaryHexo is a good SSG system with which you can achieve good results in a short time and which leaves enough room for your own creative ideas. The documentation needs a lot of improvement, especially regarding the details in depth and enriched with examples. I’m not surprised that Hexo seems to lag behind other systems like Hugo or 11ty. Maybe this also have something to do with communication. It’s not as much of a conversation in the Western space as others. In the future, I have planed to be more involved in matters of documentation and communication, even though I have not yet understood the full complexity of the substructure. However, I enjoy working with Hexo despite or because of the shortcomings. Coding without a challenge is like driving on a highway … boring :) My workflows are currently not really straightforward, especially with regard to connectivity. However, this is also due to the fact that there are no reactive components working on “my” web server. It’s all still way too much tinkering, but it will be fun over the next few months to edit all the remarks in the “IS THERE AN EASIER WAY?” blocks above and continue to develop the blog.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"Mastodon simply explained","subtitle":"Why it is not Twitter and never will be","date":"2022-11-15","updated":"2022-11-15","path":"post/Mastodon-simply-explained/","permalink":"https://kiko.io/post/Mastodon-simply-explained/","excerpt":"This weekend I had a revival experience. I got myself a Mastodon account, after many years of staying away from social media as far as I could and at most tweeting a new blog post. Since my active Twitter days, things have taken such a turn for the worse, that I’ve preferred to stay away completely. I don’t know what made me join Mastodon on Sunday, but it sparked something in me: The love for the good old internet of the late 90s, early 2000s. These guys gave me a warm welcome and made me feel that Internet communities, away from sleazy influencers and banner ads, could actually still work. I got a taste of it, when I let this blog become part of IndieWeb over a year ago, but didn’t quite believe in it because of the low traffic and mentions. Now the richest man on the planet has to come along and buy the bird site, and a niche product called Mastodon, well known but underestimated in tech circles, almost explodes under the onslaught of disappointed commercial users. And that’s what we are on Twitter, Facebook and Co … a commercial object. The problem now will be how the community can integrate these many users for whom it was not mentally prepared. What now adapts to whom? The most important thing, however, will be to teach the newcomers (and I count myself among them) that Mastodon is not Twitter in new clothes! I was going to write a few things about this, but this morning I read a Twitter thread from Peter Jakobs, a self-proclaimed digital Aboriginal from Germany, that just sums it all up and explains it so well I could hardly do it: That’s why I’m now doing something I’ve never done before: I quote Peter here completely, translated from German into English … Curtain up (and thank you DeepL :D):","keywords":"weekend revival experience mastodon account years staying social media tweeting blog post active twitter days things turn worse ive preferred stay completely dont made join sunday sparked love good internet late 90s early 2000s guys gave warm feel communities sleazy influencers banner ads work taste part indieweb year ago didnt low traffic mentions richest man planet buy bird site niche product called underestimated tech circles explodes onslaught disappointed commercial users facebook … object problem community integrate mentally prepared adapts important thing teach newcomers count clothes write morning read thread peter jakobs self-proclaimed digital aboriginal germany sums explains im quote translated german english curtain deepl","text":"This weekend I had a revival experience. I got myself a Mastodon account, after many years of staying away from social media as far as I could and at most tweeting a new blog post. Since my active Twitter days, things have taken such a turn for the worse, that I’ve preferred to stay away completely. I don’t know what made me join Mastodon on Sunday, but it sparked something in me: The love for the good old internet of the late 90s, early 2000s. These guys gave me a warm welcome and made me feel that Internet communities, away from sleazy influencers and banner ads, could actually still work. I got a taste of it, when I let this blog become part of IndieWeb over a year ago, but didn’t quite believe in it because of the low traffic and mentions. Now the richest man on the planet has to come along and buy the bird site, and a niche product called Mastodon, well known but underestimated in tech circles, almost explodes under the onslaught of disappointed commercial users. And that’s what we are on Twitter, Facebook and Co … a commercial object. The problem now will be how the community can integrate these many users for whom it was not mentally prepared. What now adapts to whom? The most important thing, however, will be to teach the newcomers (and I count myself among them) that Mastodon is not Twitter in new clothes! I was going to write a few things about this, but this morning I read a Twitter thread from Peter Jakobs, a self-proclaimed digital Aboriginal from Germany, that just sums it all up and explains it so well I could hardly do it: That’s why I’m now doing something I’ve never done before: I quote Peter here completely, translated from German into English … Curtain up (and thank you DeepL :D): Keyword “Mastodon for Twitterer” 🧵 I’ve now read from a few of you that Mastodon is too complicated, you’ve had problems, or you somehow don’t like it.As a stone age Twitterer (I created this account on Feb 13, 2007), I have by now half of my activities to Mastodon. Many of my posts you see here now come from there and I mostly come here to respond to your replies. So it is possible for an old white guy like me to move, and maybe I can ease some of your fears and help with the move. You ask why you should and that almost nothing has changed here (yet)? Maybe, but the signs on the wall don’t bode well. Elon Musk is, if you look at it neutrally, someone who destroys things in order to rebuild them - better, in his sense.But that means change is coming. It will disturb you here, but it will come here maybe not in one fell swoop like a move to Mastodon. I’m always a friend of bringing about change myself, because then I can shape it in my own way and am not dependent on others. But it could be that you will still like Twitter in 12 or 18 months. I know this myself, my Twitter experience is by no means as negative as many others say.But many others, like me, go a different way, and are thus no longer accessible here. But that is exactly one of the biggest disadvantages of Twitter and one of the advantages of Mastodon.Let me explain. Many write that they are confused by these “instances” (some say “groups” which is not quite true) on Mastodon.I can understand that, it can be confusing. But what if I told you that there is also an instance on Twitter, but just exactly one. Twitter.com namely. This one can’t communicate with anything or anyone else, an island in the middle of a vast social media ocean. And now this island has been stupidly bought by someone whose idea of what such an island should look like is rather special. Mastodon has many such islands and they are connected with bridges. You choose an island, pitch your tents there and - you are not tied to this island. On the one hand you can communicate completely freely with all inhabitants of all other islands, on the other hand you can, if you want to, take down your tents again and move to another island (you will lose a little bit, but not as much as if you want to leave here [at Twitter]). These islands (instances) are of different sizes. The biggest ones in Germany are mastodon.social, troet.cafe, mastodon.green and a few others. But there are also tiny islands, I know more than one with a single inhabitant. Is he isolated on his island? No, because he is connected with all the others via said bridges. Every island has one (or more) chiefs who keep order (Twitter does too, but we’ve complained about that a lot). These chiefs are often private individuals who run an instance as a hobby, but with the large instances there is usually a team behind it, and some also a small company (mastodon.social, for example, is run by a limited liability company that was founded by the chief developer of the Mastodon software, mastodon.green is, as far as I know, a one-man limited liability company). So now we know that there are many islands, that we are not trapped on these islands and that we can move.These islands are one of the standout features of Mastodon: a Musk, a Trump, or even a certain four-letter newspaper can buy an island or even build an island themselves, but they can’t possibly buy all the islands, simply because it’s so cheap to build new islands. (I read somewhere once, to run an instance costs about 1€ per user per month). But because the interesting thing about Twitter, like Mastodon, is not the medium itself, but the networks we build on it, it doesn’t matter what happens to individual islands as long as we can move freely. If individual islands do not want to comply with the rules at all, then they are “defederated”, or figuratively: the bridges there are demolished. However, this is the decision of each individual island chief, so it may not be grassroots democracy, but it is distributed. Ok pjakobs, enough of the chatter, how am I supposed to find out which island is suitable for me?Here is a searchable list of Mastodon instances: instances.social/list It is possible that another aspect of the “island” image will apply to you: there is a local timeline on the islands, which is also called that. So, if you choose an island that is particularly suited to a geographical region, a topic or a language, then you will see the contributions of the other “inhabitants of this island” in their local timeline - which hopefully have a certain thematic cohesion. However, as already mentioned, you can follow all people on all instances and you will also see them in your own timeline. Speaking of timelines: they are chronological and you only see the posts (toots) of those you follow. No algorithm, no “others like this too”, no good morning tweets in the late evening. Just the way Twitter used to be 10 years ago. Follow: here is one of the points that bothers me the most, even if it is a small thing. Currently there are two software versions of Mastodon in use: 3.5.3 and four, which seem to handle following a bit differently. In one (I assume the older version), if you want to follow an account on another instance, you have to specify your own handle again (in my case “@pjakobs@mastodon.green“ - but that’s no longer the case in the new version, there you click a follow button and you follow the account. Another thing that is very very different is DMs. Here on Twitter, we’re used to DMs essentially working like little emails or a messenger: we have our own inbox and that’s where the dialogue with the other person shows up. Feels good, not visible to others, because yes, in a completely different realm.In Mastodon, direct messages are simply toots that are displayed only to the recipient(s) - and normally among all the other interactions. It’s confusing and doesn’t feel as nicely protected as it does here, and as I walked around town a bit yesterday I realized: this is intentional. Twitter creates an apparent privacy through display that DMs don’t offer.DMs are deliberately not PM, not private messages. Mastodon also makes that clear through its presentation. I have therefore added my Threema and Telegram link to my profile, these tools actually offer private conversations. Is not comfortable but honest. You have created an account, but do not want to move 100% yet or, as in my case, still have many friends on this site? There is also a kind of “bridge” from Mastodon archipelago to Twitter. You can mirror your tweets to Mastodon or vice versa. This is only valid for tweets, not for replies. That means you have to be a bit careful that you don’t just spam your followers in the other network, but also interact with them. After a few days of mirroring from Twitter to Mastodon I turned the bridge around, today probably 80% of the tweets you see from me and the non-replies are on Mastodon. I use crossposter.masto.donte.com.br for this. The lovely people from @digitalcourage not only run a Mastodon instance themselves, but also put together a great overview of the Fediverse (the federated social media universe, which is much more than just Mastodon). In summary, if Musk is really fucking this up, you don’t have to be a helpless victim of his actions. Even if Mastodon might not be an alternative for you at the moment, create an account, try to find your followers over there. A life raft is not as comfortable as a floating boat, but it’s better than a sinking boat! Ah, here’s another more complete overview of Mastodon:the-federation.info/mastodon (takes a little while to load) --- Peter Jakobs You will find Peter Jakobs on this island: @pjakobs@mastodon.green. My new social media residencies are: @kiko@indieweb.social for Tech Stuff in English and @kiko@hessen.social for politics and society in German. Happy Tooting !","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Social Media","slug":"Social-Media","permalink":"https://kiko.io/tags/Social-Media/"},{"name":"Mastodon","slug":"Mastodon","permalink":"https://kiko.io/tags/Mastodon/"}]},{"title":"Syndicate Mastodon Hashtags in your favorite Feed Reader","subtitle":null,"date":"2022-11-13","updated":"2022-11-13","path":"post/Syndicate-Mastodon-Hashtags-in-your-favorite-Feed-Reader/","permalink":"https://kiko.io/post/Syndicate-Mastodon-Hashtags-in-your-favorite-Feed-Reader/","excerpt":"Ok, I admit it: I read RSS feeds. Quite old school you might think, but I’m mostly off Social Media and the most news sites quite a while ago, with a few exceptions. I just want to read selective stuff, especially in the direction of technology, and not interrupted by items, the news provider think I have to read. My favorite tool for my feed collection is Feedly, which I open up almost every morning. Today, Max Böck gave me the momentum with his article The IndieWeb for Everyone to try another type of social media I know for quite a long time, but never give it a chance: Mastodon. I’m now part of it on indieweb.social.","keywords":"admit read rss feeds school im social media news sites ago exceptions selective stuff direction technology interrupted items provider favorite tool feed collection feedly open morning today max böck gave momentum article indieweb type long time give chance mastodon part indiewebsocial","text":"Ok, I admit it: I read RSS feeds. Quite old school you might think, but I’m mostly off Social Media and the most news sites quite a while ago, with a few exceptions. I just want to read selective stuff, especially in the direction of technology, and not interrupted by items, the news provider think I have to read. My favorite tool for my feed collection is Feedly, which I open up almost every morning. Today, Max Böck gave me the momentum with his article The IndieWeb for Everyone to try another type of social media I know for quite a long time, but never give it a chance: Mastodon. I’m now part of it on indieweb.social. And as you just do … you surf around a bit on it and read a few things until a post by Matthias Ott caught my attention: Ok, nice … but does this work for other Mastadon things also, like hashtags? I have to say that I am currently very interested in creating PWAs and the techniques behind them, and I’m always looking for new resources to read. So it didn’t take me 5 minutes to start reading posts in Mastodon with the hashtag #pwa almost automatically. What can I say? It works… Step 1Search for an hash tag, f.e. #pwa. It will lead to an Url like this, depending on your Mastodon server: https://indieweb.social/web/tags/pwa Step 2Cut out /web Step 3Add .rss … and you got your feed on a special topic … … you can add to your favorite feed reader. Happy Reading…","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Mastodon","slug":"Mastodon","permalink":"https://kiko.io/tags/Mastodon/"}]},{"title":"Anatomy of Service Worker Communication","subtitle":"Let your App communicate with its Service Worker","date":"2022-11-12","updated":"2022-11-12","path":"post/Anatomy-of-Service-Worker-Communication/","permalink":"https://kiko.io/post/Anatomy-of-Service-Worker-Communication/","excerpt":"I have a SPA that works as a PWA, which means that in the background a service worker makes sure that the required files for the offline mode end up in the cache. From time to time I also update the Service Worker, which defines which files it should keep offline and which not. Unfortunately, the app itself didn’t get any of this because there was no communication channel for them to talk. If you research this topic on the web, you have to dig through many architecture pages and documentations that have one thing in common: sometimes they just don’t get to the point. So here are my 50 cents on the subject and my sample implementation.","keywords":"spa works pwa means background service worker makes required files offline mode end cache time update defines app didnt communication channel talk research topic web dig architecture pages documentations thing common dont point cents subject sample implementation","text":"I have a SPA that works as a PWA, which means that in the background a service worker makes sure that the required files for the offline mode end up in the cache. From time to time I also update the Service Worker, which defines which files it should keep offline and which not. Unfortunately, the app itself didn’t get any of this because there was no communication channel for them to talk. If you research this topic on the web, you have to dig through many architecture pages and documentations that have one thing in common: sometimes they just don’t get to the point. So here are my 50 cents on the subject and my sample implementation. Preliminary ThoughtsIn most examples I’ve read, the authors talk about code for the app itself and code for the Service Worker, but that falls short to me, because in my opinion there are THREE parts: The App The Service Worker The Service Worker Management, which takes care of the proper registration and installation of the service worker The last part of the code shouldn’t be part of the app itself, in the meaning of SoC (Seperation of Concerns). It does not contribute to the functioning of the app. The AppHere’s the general anatomy of my App: app.js123456789101112131415161718192021var app = { 'settings': { // some stuff on app settings ... }, 'pages': { // views of the SPA ... }, 'ui': { // some UI helper 'dialog': function(msg, title) { // show the message in a toast, popup or elsewhere } }, 'starter': { 'init': { // initializing of the app ... } }}window.app = app;app.starter.init(); Nothing unlikely, as I guess. It is built according to the composite pattern and has one entry point, that is called at the end of the file after it is instantiated. The Service WorkerThe Service Worker lives in its own JS file and its implementation is really straight forward: service-worker.js12345678910111213141516171819202122232425262728var cacheName = 'my-apps-cache-v1.2.3';var appFiles = [ // list of app relating files, that has to be handled]var excludeUrls = [ // list if URL's that shouldn't be handled]self.addEventListener('install', function(e) { // on install, open the cache and add all appFiles ... //activate immediatly and dont wait for connected clients to disconnect self.skipWaiting();}self.addEventListener('activate', function(e) { // remove old caches //say to all clients: Now I'm responsible self.clients.claim();}self.addEventListener('fetch', function(e) { // intercept requests and serve the app files out of the cache} I won’t go into the depth of my implementation here now, since it doesn’t matter for the message exchange. It is only good for you to know that I have versioned the cache name to exchange it with new versions. The Service Worker ManagementNow the management code for the Service Worker. It has to be loaded with the app code, because it is client code and later on it needs knowledge of the app. sw-management.js1234567891011121314151617181920212223242526272829303132if('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js') .then(function(registration) { // detect Service Worker update available registration.addEventListener('updatefound', function() { if (registration.installing) { // detect install of new Service Worker registration.installing.addEventListener('statechange', function() { if (registration.waiting) { if (navigator.serviceWorker.controller) { // there is an existing controller that has been updated //TODO: Send a message to the app } else { // first install } } } } } }} Lets Communicate…In this setup, the communication code can be implemented. Let’s do it as a round trip. 1. Client sends message to Service WorkerAs sw-management.js represents our Service Worker management code, we add a little function to send a message in here: sw-management.js1234567function sendMessageToServiceWorker(type, msg) { if (navigator.serviceWorker.controller) { navigator.serviceWorker.controller.postMessage( {'type': type, 'msg': msg } ); }} We define a type for the purpose of our communcation and a message itself. The function can be called wherever, like this: 1sendMessageToServiceWorker("TEST", "Hey, Service Worker"); 2. Service Worker receives the messageIn the Service Worker code we need to add a recipient: service-worker.js1234567self.addEventListener('message', function(event) { if (!event.data) return; if (event.data.type === 'TEST') { // do something regarding to the type and/or with the message }} 3. … and sends a message back to the clientWhat we want to do with the message depends what we want to achive, but in this example, just let’s greet back: service-worker.js12345678910111213141516171819function sendMessageToClients(type, msg) { // as the SW can control multiple clients, we have to catch them all self.clients.matchAll({ includeUncontrolled: true }) .then(function(clients) { for (const client of clients) { client.postMessage( {'type': type, 'msg': msg } ); } });}self.addEventListener('message', function(event) { if (!event.data) return; if (event.data.type === 'TEST') { sendMessageToClients(event.data.type, 'Hi Clients...'); }} 4. Client receives the answer from the Service WorkerIn order to get messages from the Service Worker, we have to implement a receiver in the client also: sw-management.js12345678910if('serviceWorker' in navigator) { //... 'navigator.serviceWorker.register' stuff navigator.serviceWorker.addEventListener('message', function(event) { if (event.data) { // do something with the message from the Service Worker } });} 5. … and shows it in the appAs I pointed out earlier, that the management code has to be loaded alongside with the app code, it’s a breeze to show the message: app.js1234567891011121314var app = { ... 'ui': { // some UI helper 'dialog': function(msg, title) { // show the message in a toast, popup or elsewhere } } ...}window.app = app;... sw-management.js12345678910if('serviceWorker' in navigator) { //... 'navigator.serviceWorker.register' stuff navigator.serviceWorker.addEventListener('message', function(event) { if (event.data) { app.ui.dialog.info(event.data.msg, 'Service Worker says...'); } });} The Update MessageWith this infrastructure, everything is there to show a message, when the Service Worker is updated: sw-management.js123456789101112131415161718192021222324252627282930313233343536if('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js') .then(function(registration) { // detect Service Worker update available registration.addEventListener('updatefound', function() { if (registration.installing) { // detect install of new Service Worker registration.installing.addEventListener('statechange', function() { if (registration.waiting) { if (navigator.serviceWorker.controller) { // there is an existing controller that has been updated app.ui.dialog.info('New version installed', 'Service Worker'); } else { // first install } } } } } } ...} Important to point out, that the Service Worker side of the communication is not involved in this case, because only the client-side management code knows when a new version has to be installed. More Info Jake Archibald: The service worker lifecycleDemian Renzulli & Andrew Guan: Two-way communication with service workersAdam Bar: Handling Service Worker updates – how to keep the app updated and stay saneFelix Gerschau: Service Worker Lifecycle ExplainedFelix Gerschau: How to communicate with Service WorkersPeter Kröner: PostMessage zwischen Service Worker und Client(s) (GERMAN)","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"SPA","slug":"SPA","permalink":"https://kiko.io/tags/SPA/"},{"name":"PWA","slug":"PWA","permalink":"https://kiko.io/tags/PWA/"}]},{"title":"Discoveries #20 - CSS & UI","subtitle":null,"series":"Discoveries","date":"2022-10-08","updated":"2022-10-08","path":"post/Discoveries-20-CSS-UI/","permalink":"https://kiko.io/post/Discoveries-20-CSS-UI/","excerpt":"Web interfaces are unthinkable without CSS. It has its pitfalls, but when used correctly it’s damn powerful. It’s always incredible what developers do with it. This month’s Discoveries is about the basics and the amazing. My Custom CSS ResetDefensive CSS10 Useful CSS Tricks for Front-end DevelopersAnimated Star RatingCSS Marquee ExamplesCSS Rolling TextCool Hover Effects That Use CSS Text ShadowSolving 'The Dangler' Conundrum with Container Queries and :has()Conditionally Styling Selected Elements in a Grid ContainerIntersection Observer Scrolling Effects","keywords":"web interfaces unthinkable css pitfalls correctly damn powerful incredible developers months discoveries basics amazing custom resetdefensive css10 tricks front-end developersanimated star ratingcss marquee examplescss rolling textcool hover effects text shadowsolving dangler conundrum container queries hasconditionally styling selected elements grid containerintersection observer scrolling","text":"Web interfaces are unthinkable without CSS. It has its pitfalls, but when used correctly it’s damn powerful. It’s always incredible what developers do with it. This month’s Discoveries is about the basics and the amazing. My Custom CSS ResetDefensive CSS10 Useful CSS Tricks for Front-end DevelopersAnimated Star RatingCSS Marquee ExamplesCSS Rolling TextCool Hover Effects That Use CSS Text ShadowSolving 'The Dangler' Conundrum with Container Queries and :has()Conditionally Styling Selected Elements in a Grid ContainerIntersection Observer Scrolling Effects My Custom CSS Reset by Josh Comeau https://www.joshwcomeau.com/css/custom-css-reset/ Browsers behave differenly out of the box regarding CSS. Therefore it is always advisable to have a CSS reset. Josh shows us his approach. He also inspired Elly to her Gist. Defensive CSS by Ahmad Shadeed https://defensivecss.dev/ Ahmad is a master of CSS and one of his concerns, which he also points out repeatedly in his blog, is to use styles defensively. He has now made his own website out of this. 10 Useful CSS Tricks for Front-end Developers by Alex Ivanovs https://stackdiary.com/useful-css-tricks/ Alex has some useful tips on writing better and smart CSS code. I have to try Shadow for transparent images as soon as possible … Animated Star Rating|IMGFILE by Jon Kantner https://codepen.io/jkantner/pen/BarvVNa Star ratings are everywhere because they encourage the user to interact with the website and may give the following important clues about how good the product is. Jon has taken them to the next level visually with his animations. CSS Marquee Examples by Ryan Mulligan https://codepen.io/hexagoncircle/full/eYMrGwW Marquee visualizations were all the rage in the 90s, but they still have their place today and are easier to implement than ever before. CSS Rolling Text by Marcello Lopes https://codepen.io/marcell0lopes/pen/oNemQmB You have to tease something with more than one verb? Try this simple solution from Marcello on scrolling text with pure CSS. Cool Hover Effects That Use CSS Text Shadow by Temani Afif https://css-tricks.com/cool-hover-effects-that-use-css-text-shadow/ Hover effects are only useful on computers with mouse support, but how cool you can design them Temani shows us on CSS Tricks. Solving 'The Dangler' Conundrum with Container Queries and :has() by Dave Rupert https://daverupert.com/2022/07/solving-the-dangler-conundrum-with-has-and-container-queries Grids are super cool, but there is the problem how to style leftovers that doesn’t fit in the matrix. Having a 3 column grid and 12 elements? Fine. But what is with the 13th element? Dave shows us how to deal it it in CSS. Conditionally Styling Selected Elements in a Grid Container by Preethi https://css-tricks.com/conditionally-styling-selected-elements-in-a-grid-container/ Elements in a grid have their style. Period. … Wait, no! It is possible to style an element regarding to its neighbour, as shown in this article, by a clever using of :nth-of-type. Intersection Observer Scrolling Effects by Jhey https://codepen.io/jh3y/pen/xxWyEBQ Codepen’s user JHey shows us how to animate elements in 5 different ways on scrolling. Pretty neat…","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"UI","slug":"UI","permalink":"https://kiko.io/tags/UI/"}]},{"title":"One mouse to rule them all","subtitle":null,"series":"Golem","date":"2022-10-08","updated":"2022-10-08","path":"post/One-mouse-to-rule-them-all/","permalink":"https://kiko.io/post/One-mouse-to-rule-them-all/","excerpt":"Multiple monitors on one computer are elementary for effective IT work. However, operating multiple computers with one input set requires tools, for example one from the Microsoft Garage. The trend is toward second or third devices, especially among IT workers. In addition to the stationary computer, which may still be under the desk, there is a laptop and possibly a tablet. All of them are connected to cloud services. If the desk is large enough, there may be two monitors and a full-size keyboard connected to the desktop, the laptop on the left, on which two or three special programs are installed, and the Surface tablet on the right for communication with its narrow-track keyboard. And spread out on the table are three mice in different colors so as not to constantly get the wrong one. A scenario that the author of these lines was also confronted with … yes, until a few years ago a small tool for Microsoft Windows fell in front of his feet, which abruptly ended the input chaos: Mouse Without Borders.","keywords":"multiple monitors computer elementary effective work operating computers input set requires tools microsoft garage trend devices workers addition stationary desk laptop possibly tablet connected cloud services large full-size keyboard desktop left special programs installed surface communication narrow-track spread table mice colors constantly wrong scenario author lines confronted … years ago small tool windows fell front feet abruptly ended chaos mouse borders","text":"Multiple monitors on one computer are elementary for effective IT work. However, operating multiple computers with one input set requires tools, for example one from the Microsoft Garage. The trend is toward second or third devices, especially among IT workers. In addition to the stationary computer, which may still be under the desk, there is a laptop and possibly a tablet. All of them are connected to cloud services. If the desk is large enough, there may be two monitors and a full-size keyboard connected to the desktop, the laptop on the left, on which two or three special programs are installed, and the Surface tablet on the right for communication with its narrow-track keyboard. And spread out on the table are three mice in different colors so as not to constantly get the wrong one. A scenario that the author of these lines was also confronted with … yes, until a few years ago a small tool for Microsoft Windows fell in front of his feet, which abruptly ended the input chaos: Mouse Without Borders. Microsoft, like many IT corporations, encourages its employees to create side projects, which then either eventually find their way into one of the main products or are released under the Microsoft Garage banner. This is also the case with the work of Truong Do, a Microsoft employee from Washington, who deals with Microsoft Dynamics in his daily business. He was probably also fed up with the numerous input devices and developed a tool more than 10 years ago with which the mouse pointer of a Windows device can be moved across its own monitor border to another device, whereby one of the keyboards is then activated on the device on which the mouse pointer is currently located. The startup is simple: Download tool via http://aka.ms/mm Make sure that all devices are available on the same network Run the installation program on all devices From one device, link the other devices using the security code generated by the program Arrange the devices on the desktop in the program by dragging and dropping them from their locations Done Other featuresBesides the main purpose of sharing mouse and keyboard across multiple devices, you can also set up a shared clipboard (Share Clipboard) and make it possible to copy files from one computer to another via drag & drop (Transfer File). This creates a special folder on the desktop of the target computer, where these files end up via the clipboard. It is also useful that the user does not become a long-distance pusher when the pointer is on the far right device and he has to go to the far left, because where it stops on the right, it continues on the far left. For keyboard lovers, Truong Do has made keyboard shortcuts available for each linked device, with a choice of F-keys or numbers. All machines can also be locked at once via Ctrl-Alt-L. If the connection hangs once, a spirited Ctrl-Alt-R helps to get them all back on track. As an extra goodie, Truong Do has included a narrow-gauge screenshot capture, but you can safely turn it off, because it can’t compete with tools like Brian Scott’s Cropper or Greenshot, but rather hinders because it uses common keyboard shortcuts. ConclusionYes, multiple large monitors and classic remote solutions like RDP or TeamViewer help to operate multiple machines at the same time, but when you have the zoo of devices physically together, working with Mouse Without Borders feels like having four or more monitors, with the advantage that the individual machines don’t have to give up any of their performance. The nice thing, too, is that it doesn’t matter at all what version of Windows is running on the devices. Everything from Windows 7 upwards is supported.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Remote","slug":"Remote","permalink":"https://kiko.io/tags/Remote/"},{"name":"Workflow","slug":"Workflow","permalink":"https://kiko.io/tags/Workflow/"}]},{"title":"Creating Icon Font from SVG Files","subtitle":null,"date":"2022-09-17","updated":"2022-09-17","path":"post/Creating-Icon-Font-from-SVG-Files/","permalink":"https://kiko.io/post/Creating-Icon-Font-from-SVG-Files/","excerpt":"A several years ago I started building a little PWA and chose Bootswatch 3.3.5. for theming. As it depends on Bootstrap I was able to use the icons from Bootstrap. At the beginning I needed only a handful of these icons, but with the time it became more and more difficult to find the right one, because the Bootstrap Glyphicons in version 3 included only around 250 icons and there was not always the right one. Also, the app was always lugging around well over 100 KB of extra files, of which I actually only needed a few kilobytes. In another project I had used Fontello, where you can build and download your own icon font from a selection of available icons. Very nice, but I didn’t feel like fiddling with project-specific configuration files on the Fontello website. But since you could upload your own SVG files in Fontello, which were then taken over into the font, the same had to work somehow with a Node.JS plugin!? And yes gulp-iconfont from Nicolas Froidure was exactly what I needed. First SolutionJust copy a bunch of SVG files in a folder, run gulp and there was my own customized icon font with a tolerable size of around 20 kilobytes. At that time, Thomas Jaggi had taken care of the creation of a CSS file with the correct code points that matched the font with his tool gulp-iconfont-css.","keywords":"years ago started building pwa chose bootswatch theming depends bootstrap icons beginning needed handful time difficult find glyphicons version included app lugging kb extra files kilobytes project fontello build download icon font selection nice didnt feel fiddling project-specific configuration website upload svg work nodejs plugin gulp-iconfont nicolas froidure solutionjust copy bunch folder run gulp customized tolerable size thomas jaggi care creation css file correct code points matched tool gulp-iconfont-css","text":"A several years ago I started building a little PWA and chose Bootswatch 3.3.5. for theming. As it depends on Bootstrap I was able to use the icons from Bootstrap. At the beginning I needed only a handful of these icons, but with the time it became more and more difficult to find the right one, because the Bootstrap Glyphicons in version 3 included only around 250 icons and there was not always the right one. Also, the app was always lugging around well over 100 KB of extra files, of which I actually only needed a few kilobytes. In another project I had used Fontello, where you can build and download your own icon font from a selection of available icons. Very nice, but I didn’t feel like fiddling with project-specific configuration files on the Fontello website. But since you could upload your own SVG files in Fontello, which were then taken over into the font, the same had to work somehow with a Node.JS plugin!? And yes gulp-iconfont from Nicolas Froidure was exactly what I needed. First SolutionJust copy a bunch of SVG files in a folder, run gulp and there was my own customized icon font with a tolerable size of around 20 kilobytes. At that time, Thomas Jaggi had taken care of the creation of a CSS file with the correct code points that matched the font with his tool gulp-iconfont-css. SetupFirst I needed some SVG files… 12345678|-- Images |-- SVG |-- alert.svg |-- cancel.svg |-- delete.svg |-- email.svg |-- flag.svg |-- link.svg … and the NPM packages of my task runner gulp.js: 12npm install --save-dev gulp-iconfontnpm install --save-dev gulp-iconfont-css For generating the CSS file a template called _icons.css is shipped with the NPM package of gulp-iconfont-css, which can be customized as needed. In my case I just made a copy into a folder called Templates, where I store all other template files I need for my PWA. Templates/icons-template.css1234567891011121314151617181920212223242526@font-face { font-family: "<%= fontName %>"; src: url('<%= fontName %>.eot'); src: url('<%= fontName %>.eot?#iefix') format('eot'), url('<%= fontName %>.woff2') format('woff2'), url('<%= fontName %>.woff') format('woff'), url('<%= fontName %>.ttf') format('truetype'), url('<%= fontName %>.svg#<%= fontName %>') format('svg');}.<%= cssClass %>:before { font-family: "<%= fontName %>"; font-style: normal; font-variant: normal; font-weight: normal; text-decoration: none; text-transform: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}<% _.each(glyphs, function(glyph) { %>.<%= cssClass %>-<%= glyph.fileName %>:before { content: "\\<%= glyph.codePoint %>";}<% }); %> As I was using Gulp for building my app, I had to integrate a new task for creating the icon font in the gulpfile.js: /gulpfile.js12345678910111213141516171819202122232425262728293031var iconfont = require('gulp-iconfont'), iconfontCss = require('gulp-iconfont-css');var distFolder = 'Build';var fontName = 'MyAppIcons'; gulp.task('create-iconfont', function(){ return gulp.src(['Images/SVG/*.svg']) .pipe(iconfontCss({ fontName: fontName, path: 'Templates/icons-template.css', targetPath: fontName + '.css', fontPath: distFolder })) .pipe(iconfont({ formats: ['svg', 'ttf', 'eot', 'woff', 'woff2'], fontName: fontName, // Required fontHeight: 1001, normalize: true, prependUnicode: true, // Recommended timestamp: Math.round(Date.now() / 1000) // Recommended })) .pipe(gulp.dest(distFolder)); });gulp.task('build', gulp.series( 'create-iconfont' // other gulp tasks )); I little explanation on that … First of all the task for gulp-iconfont-css has to be inserted before piping the files through gulp-iconfont, in order to create the CSS file properly. Following options were used: gulp-iconfont-css Option Description fontName Name that the generated font will have path Path to the template for generating CSS file targetPath Path where the CSS file will be generated fontPath Path to the icon font file gulp-iconfontThe library combines some other projects such as svgicons2svgfont, gulp-svgicons2svgfont and gulp-svg2ttf to create the different font formats, because its just a wrapper around them. Option Description formats Font file formats that will be created fontName Name of the font (for svgicons2svgfont) fontHeight Minimum height on scaling icons normalize Normalize icons by scaling them to the height of the highest icon (for svgicons2svgfont) prependUnicode Prefix files with their automatically allocated unicode code point (for gulp-svgicons2svgfont) timestamp Get consistent builds when watching files (for gulp-svg2ttf) OutputAfter running gulp build all files needed were generated: 1234567|-- Build |-- MyAppIcons.css |-- MyAppIcons.eot |-- MyAppIcons.svg |-- MyAppIcons.ttf |-- MyAppIcons.woff |-- MyAppIcons.woff2 MyAppIcons.css1234567891011121314151617181920212223242526272829303132333435363738394041424344@font-face { font-family: "MyAppIcons"; src: url('MyAppIcons.eot'); src: url('MyAppIcons.eot?#iefix') format('eot'), url('MyAppIcons.woff2') format('woff2'), url('MyAppIcons.woff') format('woff'), url('MyAppIcons.ttf') format('truetype'), url('MyAppIcons.svg#MyAppIcons') format('svg');}.icon:before { font-family: "MyAppIcons"; font-style: normal; font-variant: normal; font-weight: normal; text-decoration: none; text-transform: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}.icon-alert:before { content: "\\E001";}.icon-cancel:before { content: "\\E002";}.icon-delete:before { content: "\\E003";}.icon-email:before { content: "\\E004";}.icon-flag:before { content: "\\E005";}.icon-link:before { content: "\\E006";} All I had to do now, was to reference the new CSS file in my HTML and decorating my HTML tags with one of the new icon-classes. Yes! My had my own icon font, just by copying some SVG files in a folder… The ProblemA while later, as my app grew, I needed some new icons, but in the meantime I had used the content codes elsewhere in static CSS files, for example with other pseudo-selectors like AFTER. style.css1234.my-special-link::after { font-family: "MyAppIcons"; content: "\\E006";} After copying a few new icon files to the SVG folder and running the build, I found that most of the icons didn’t fit anymore!? After a short research it was clear to me what had happened. With the insertion of the new SVG files, the order of the files in the folder was changed. But since the libraries processed this folder in alphabetical order, file by file, and simply incremented the codes to be assigned, the codes had simply shifted. In the example above, this can be easily understood if we insert a file named cloud.svg here. The code for cancel.svg (E002) remains untouched, but cloud.svg now gets E003 and delete.svg E004 and so on. The hardwired icon for .my-special-link (E006) now showed instead of the link icon a flag icon. Newer SolutionI don’t know when this happened, because I don’t keep a constant eye on my little PWA and thus the further development of the related tools, but after having manually adjusted the moved codes two or three times now and once again wanting to change a few things on the PWA’s icons, I had enough and came across the following sentence on the gulp-iconfont-css page on GitHub: Recent versions of gulp-iconfont emit a glyphs (or codepoints < 4.0.0) event (see docs) which should likely be used instead of the workflow described below. However, it will continue to work as expected. --- backflip (Thomas Jaggi) Ahh, ok. I don’t need the gulp-iconfont-css anymore. I can create the CSS file by myself. Lets see how the gulp-iconfont task looks, after rewriting: /gulpfile.js1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950var iconfont = require('gulp-iconfont');var distFolder = 'Build';var fontObj = { fontName: "MyAppIcons", cssClass: "icon"};gulp.task("create-iconfont", function () { return gulp .src(["Images/SVG/*.svg"]) .pipe( iconfont({ fontName: fontObj.fontName, fontHeight: 1001, formats: ["svg", "ttf", "eot", "woff", "woff2"], normalize: true, prependUnicode: true, timestamp: Math.round(Date.now() / 1000), }) ) .on("glyphs", function (glyphs, options) { fontObj.glyphs = glyphs.map(mapGlyphs); console.log(fontObj, options); gulp .src('Templates/icons-template.css') .pipe(consolidate("lodash", fontObj)) .pipe(rename({ basename: fontObj.fontName })) .pipe(gulp.dest(distFolder)); }) .pipe(gulp.dest(distFolder));});function mapGlyphs(glyph) { return { fileName: glyph.name, codePoint: glyph.unicode[0].charCodeAt(0).toString(16).toUpperCase(), };}gulp.task('build', gulp.series( 'create-iconfont' // other gulp tasks )); First I have introduced a new fontObj variable to hold all informations for the untouched CSS template. The major difference is now, that iconfont is the only main task, with a subtask where the glyphs are processed directly via consolidate. The mapGlyphs() function ensures that the file names with the CodePoints are transferred into an easily usable structure. Works fine, but doesn’t solve my problem with the shifted CodePoints, when inserting new SVG icons … but someone remarked in a StackOverflow article to have a look at the test files of gulp-iconfont. There, the SVG files carry as prefix the name of the CodePoints to be used for this file!. Unfortunately, this feature is not documented, but it works… 123456789|-- Images |-- SVG |-- uE001-alert.svg |-- uE002-cancel.svg |-- uE003-delete.svg |-- uE004-email.svg |-- uE005-flag.svg |-- uE006-link.svg |-- uE007-cloud.svg Now the order of the SVG files doesn’t matter anymore. I just copy a new SVG file into the folder and rename it with a currently unused Unicode. Upcoming Solution: Using SVG SpritesInterestingly, the inventor of gulp-iconfont page writes on the GiHub page: Warning: While this plugin may still be useful for fonts generation or old browser support, you should consider using SVG icons directly. Indeed, when i created gulp-iconfont and all its related modules, using SVG icons was just not realistic for a wide browser suppport but i was already conviced that SVG was the future, that’s why i wanted my SVG source files to sit separated in a folder. So, now, just enjoy switching to SVG with almost no effort :). Was a great open source journey with you all! --- Nicolas Froidure (nfroidure) This goes along with some older articles of Chris Coyer from CSSTricks: https://css-tricks.com/svg-sprites-use-better-icon-fonts/ https://css-tricks.com/svg-symbol-good-choice-icons/ https://github.com/w0rm/gulp-svgstore Maybe it’s time to say goodbye to icon fonts completely and use SVG files directly, but in my opinion this is not suitable for all needs. Especially if you work a lot with pesudo selectors, a general rebuild of the code and the UI will be necessary and you would do well to weigh the cost-benefit carefully.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Bundling","slug":"Bundling","permalink":"https://kiko.io/tags/Bundling/"},{"name":"SVG","slug":"SVG","permalink":"https://kiko.io/tags/SVG/"},{"name":"Font","slug":"Font","permalink":"https://kiko.io/tags/Font/"}]},{"title":"Dopamine, a music player for Windows 10 as it should be","subtitle":"A minimalist MP3 player under Windows, distributed as open source, which does not shy away from large collections","series":"Golem","date":"2022-08-21","updated":"2022-08-21","path":"post/Dopamine-a-music-player-for-Windows-10-as-it-should-be/","permalink":"https://kiko.io/post/Dopamine-a-music-player-for-Windows-10-as-it-should-be/","excerpt":"There are about fourleventy millions music apps for smartphones running Android and iOS. However, most of them are relatively junk or try to foist malware on users. You have to make sure that you separate the wheat from the chaff. On the other hand, the situation is surprisingly different for Windows, the much older operating system. The older ones of us will still remember the glorious WinAmp times, whose current owner Radionomy has been making a very long new attempt for a new version 6 since 2018 (version 5.8 is already a handsome 6 years old), but you can actually count the good music players for Windows 10 and higher on one hand, if you subtract the streaming apps such as Spotify and Co. and disregard everything that comes along as a jack of all trades and can ALSO play MP3. The best known are the in Windows included and miserably failed iTunes clone from Micosoft called Groove, AIMP, foobar2000, MediaMonkey and MusicBee. Some nostalgic people might also add the good old Windows Media Player, which managed to survive on the net despite Groove. If you look at the download pages of these music player candidates and try to look behind the business model, some of them simply do not download. One or the other player also overdoes it with the featuritis. Bouncing balls or bars to the music are gimmicks that were thought to be outdated long ago, when it is actually only about listening to music. Belgian software developer Raphaël Godart (twitter.com/RaphaelGodart) must have felt the same way a few years ago when he set out to launch his own player for local MP3 collections, which in this case sounds falsely commercial because his player Dopamine is freely available on GitHub and open source under GPL 3.0 license. It plays music under a plain and simple, yet chic interface … Period. Everything a music lover’s heart desires is on board:","keywords":"fourleventy millions music apps smartphones running android ios junk foist malware users make separate wheat chaff hand situation surprisingly windows older operating system remember glorious winamp times current owner radionomy making long attempt version handsome years count good players higher subtract streaming spotify disregard jack trades play mp3 included miserably failed itunes clone micosoft called groove aimp foobar2000 mediamonkey musicbee nostalgic people add media player managed survive net download pages candidates business model simply overdoes featuritis bouncing balls bars gimmicks thought outdated ago listening belgian software developer raphaël godart twittercom/raphaelgodart felt set launch local collections case sounds falsely commercial dopamine freely github open source gpl license plays plain simple chic interface … period lovers heart desires board","text":"There are about fourleventy millions music apps for smartphones running Android and iOS. However, most of them are relatively junk or try to foist malware on users. You have to make sure that you separate the wheat from the chaff. On the other hand, the situation is surprisingly different for Windows, the much older operating system. The older ones of us will still remember the glorious WinAmp times, whose current owner Radionomy has been making a very long new attempt for a new version 6 since 2018 (version 5.8 is already a handsome 6 years old), but you can actually count the good music players for Windows 10 and higher on one hand, if you subtract the streaming apps such as Spotify and Co. and disregard everything that comes along as a jack of all trades and can ALSO play MP3. The best known are the in Windows included and miserably failed iTunes clone from Micosoft called Groove, AIMP, foobar2000, MediaMonkey and MusicBee. Some nostalgic people might also add the good old Windows Media Player, which managed to survive on the net despite Groove. If you look at the download pages of these music player candidates and try to look behind the business model, some of them simply do not download. One or the other player also overdoes it with the featuritis. Bouncing balls or bars to the music are gimmicks that were thought to be outdated long ago, when it is actually only about listening to music. Belgian software developer Raphaël Godart (twitter.com/RaphaelGodart) must have felt the same way a few years ago when he set out to launch his own player for local MP3 collections, which in this case sounds falsely commercial because his player Dopamine is freely available on GitHub and open source under GPL 3.0 license. It plays music under a plain and simple, yet chic interface … Period. Everything a music lover’s heart desires is on board: Automatic reading of a folder configured at the beginning. Display of the collection by artist, genre, album, title or folder Playlist management Light and dark theme, including setting the accent color Interface in 30 languages Integration with taskbar and notification bar Automatic updates tns({ container: \"#image-slide-zta3zg\", items: 1, slideBy: \"page\", controls: false, nav: true }); The design of the application is instantly convincing. Everything is limited to the most necessary, an accent color and a lot of white e.g. gray space, so that not only the ear feels comfortable with the music, but also the eye. And all this at a very good speed and nicely animated transitions from one view to another. Besides the basic ability to play MP3 files, the author has also included a rudimentary, but well-functioning editing function of the MP3 metadata. Furthermore, any star assignments or added lyrics are stored directly in the file and not just in the SQLite database behind it. However, Godart has also included a few useful things from the Featuritis department in its program. On the one hand, it downloads information from last.fm about the currently played artist and you can scrobble there if you like, and on the other hand, it scans some lyric collections for the lyrics, which unfortunately rarely works. Quite unique is one of the most recently added features: a blacklist! Yes, many an album of a favorite musician contains a track that he would have been better off sparing himself. Such pieces can now be specifically hidden, so that the musical enjoyment is not spoiled. Further developmentNew releases of Dopamine 2 since version 2.0.8 are not as frequent as before, because the program is basically developed out and Raphaël only adds smaller features, translations and bugfixes. Instead, 4 years after the release of the first version two years ago, he has embarked on a completely new development with version 3. Where Dopamine up to version 2 was developed with Microsoft C# and WPF (Windows Presentation Foundation), the latest will be based on Electron, Angular and Typescript, i.e. packaged web technology (HTML, CSS and JavaScript) for all operating systems and not just Windows. Currently, Godart has reached Preview 10, which has by far not the same scope of the previous version, but is already running stable. Music in the cloudIf you’re on the go and have stored your music on a cloud service like Microsoft’s OneDrive, you might pay attention to the fact that on a device connected to it, the entire collection is not downloaded, but only selected parts in order to save local storage space. Microsoft makes it easy for users there: it displays all OneDrive files in Windows Explorer with 0 bytes of space used and only downloads them when they are accessed or when the user selects Always keep on this device in a folder’s context menu. This works quite beautifully with Dopamine 2. It, with support from C# and the operating system, simply ignores all those files that are not available on the disk in real terms when scanning the specified music folder. Dopamine 3, on the other hand, in which the scan must take place via Node.js and the fs.readdir() method, immediately triggers the download function of OneDrive and you unintentionally fill up your disk. Curiosity on the sideRaphaël’s C#/WPF solution weighs 76 MB when installed, 2.5 MB of it for the executable, the new Electron-based one under Windows, however, already 302 MB, with the EXE being a whopping 128 MB. If you take a look at the code base, this becomes even more obvious: while the Windows-only version comes along with 760 files, the new Electron version already contains 86,092 files in 7,948 folders with a total weight of almost one gigabyte after running npm install! Once again you can see that platform independence in the brave new Node.JS world is often bought with heavy weight. This is mainly due to the necessity (or the unkindness, depending on how you look at it) to drag along thousands of dependent libraries (dependencies), which occasionally only bring a string to a certain width or something similarly trivial. Not to mention the problems with depublicated or malware-infested dependencies, which have recently upset large parts of the community. ConclusionDopamine 2 is a really fantastic music player for Windows that simply outshines most others due to its simplicity and well-done interface. Let’s see if Godard manages to do something similar with version 3 and Electron. It’s a challenge, but he seems to be a full-blooded developer. Chapeau Raphaël…","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Audio","slug":"Audio","permalink":"https://kiko.io/tags/Audio/"}]},{"title":"Scandinavian Presets for Lightroom","subtitle":null,"series":"Lightroom Presets","date":"2022-07-19","updated":"2022-07-19","path":"post/Scandinavian-Presets-for-Lightroom/","permalink":"https://kiko.io/post/Scandinavian-Presets-for-Lightroom/","excerpt":"It doesn’t matter what time of year you go to Scandinavia. Countries like Denmark or Norway have a Nordic charm that you can never really escape. But it doesn’t always have to be the far north that provides fascinating motifs for photographers. With these Lightroom presets you can enhance your images with the Nordic feel.","keywords":"doesnt matter time year scandinavia countries denmark norway nordic charm escape north fascinating motifs photographers lightroom presets enhance images feel","text":"It doesn’t matter what time of year you go to Scandinavia. Countries like Denmark or Norway have a Nordic charm that you can never really escape. But it doesn’t always have to be the far north that provides fascinating motifs for photographers. With these Lightroom presets you can enhance your images with the Nordic feel. Scandinavian City NightsWhen it gets late in the city, the artificial light bathes them in a warm veil. The shadows are deeper and the colors are more concise.Time to get a drink. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-qood4y\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scandinavian City Nights.xmp Scandinavian Blue HourAll over the world, the Blue Hour is a special time of the day. The light is fading away slightly and everything shines in magic colors. Let them shine… var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-t2dfug\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scandinavian Blue Hour.xmp Scandinavian ColorsNordic nature is far from being as rich and colorful as that in the south, but it has its own charm and with color you can help a little. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-ia9xt5\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scandinavian Colors.xmp Scandinavian DramaNot only since Shakespeare and his Hamlet, we know about the dramas of the Nordic sagas. The landscape itself is dramatic and the stories are set in it. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-bb7m1z\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scandinavian Drama.xmp Scandinavian SeascapeThe sea plays a big role in the Nordic countries, as they have very long coasts. No one who travels there with a camera manages to resist this beauty. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-2r7tza\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scandinavian Seascape.xmp","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Presets","slug":"Presets","permalink":"https://kiko.io/tags/Presets/"}]},{"title":"Gitpod - Visual Studio Code on the Web","subtitle":"The popular code editor is conquering the browser and remote work and the Kiel-based company Gitpod is at the forefront with their solution","series":"Golem","date":"2022-07-17","updated":"2022-07-17","path":"post/Gitpod-Visual-Studio-Code-on-the-Web/","permalink":"https://kiko.io/post/Gitpod-Visual-Studio-Code-on-the-Web/","excerpt":"It’s amazing how quickly the editor Visual Studio Code (VS Code) has conquered the developer community (#1 in the Stack Overflow Developer Survey Ranking 2021) and even those from the Linux faction, who are historically rather critical of Microsoft, but for good reason. The company from Redmond has done quite a lot right with the tool and has gathered a large group of open source developers around it (currently 1,640 contributors), who contribute to the fact that the Swiss Microsoft team around Erich Gamma can bring out a new release for Windows, Linux and macOS every few weeks. It all started with the Monaco Editor, the core around which VS Code is built and which was first released on April 14, 2016. The exciting thing about Monaco and VS Code is, that it is consistently written using web technologies, i.e. HTML, CSS and Javascript, packaged and executed using the framework Electron developed by GitHub, which in turn is based on Node.js and the open source browser engine Chromium from Google.","keywords":"amazing quickly editor visual studio code conquered developer community #1 stack overflow survey ranking linux faction historically critical microsoft good reason company redmond lot tool gathered large group open source developers contributors contribute fact swiss team erich gamma bring release windows macos weeks started monaco core built released april exciting thing consistently written web technologies html css javascript packaged executed framework electron developed github turn based nodejs browser engine chromium google","text":"It’s amazing how quickly the editor Visual Studio Code (VS Code) has conquered the developer community (#1 in the Stack Overflow Developer Survey Ranking 2021) and even those from the Linux faction, who are historically rather critical of Microsoft, but for good reason. The company from Redmond has done quite a lot right with the tool and has gathered a large group of open source developers around it (currently 1,640 contributors), who contribute to the fact that the Swiss Microsoft team around Erich Gamma can bring out a new release for Windows, Linux and macOS every few weeks. It all started with the Monaco Editor, the core around which VS Code is built and which was first released on April 14, 2016. The exciting thing about Monaco and VS Code is, that it is consistently written using web technologies, i.e. HTML, CSS and Javascript, packaged and executed using the framework Electron developed by GitHub, which in turn is based on Node.js and the open source browser engine Chromium from Google. It is not really surprising that the developers have built a really good editor from the start, which could directly defy the first Chromium-based tool Brackets from Adobe at that time, because Erich Gamma was the head of the development environment Eclipse for many years and therefore knows about the needs of the worldwide developer community. Surprisingly however is already, that although VS Code is based on Web technologies, it took some years, until the editor made it out of its Electron cardboard, directly into the Browser. For example, it wasn’t until 2020, 2 years after Microsoft acquired GitHub, that they announced github.dev, which is just a call target for the newly introduced Magic Dot and opens a project hosted on GitHub in the browser. For those who like to try it out, just go to any GitHub repository and hit the DOT key and the project will open in a VS Code browser window. A variant of this is vscode.dev, which can open not only GitHub projects, but any from your own hard drive. However, both browser editors have one thing in common: you can code wonderfully on the go, but you cannot start the projects and thus also not validate your own code for executability. The substructure is missing and is therefore not a real IDE. No pre-processing via Grunt or Gulp, no starting web server or similar, simply because there is no console that could communicate with the operating system. However, Microsoft has also announced another tool in 2020 that is supposed to bring exactly this substructure via cloud container: GitHub Codespaces. Beta access has been expanding for a few weeks now, and it should be available for team and enterprise cloud plans starting in August. Cloud-based development environment from Kiel, Germany: GitpodA team around Sven Efftinge and Anton Kosyakov from the beautiful city of Kiel, asked themselves the question why not build an online development environment back in 2017 and launched the project Theia under the umbrella of the Eclipse Foundation. The idea was to develop a remote-first IDE that runs both locally and in the browser and fully supports the numerous VS Code extensions already available. They named the actual product around Theia Gitpod (gitpod.io). Although there are still numerous Theia solutions available today, such as Eclipse Che, Stackblitz or the Google Cloud Shell Editor, Gitpod decided to move to the VS Code platform in late 2020 after Erich Gamma and his team provided Remote Support in VS Code, on which github.dev is based, among others. However, since Microsoft had decided not to open source the server component for the time being (the commercial GitHub CodeSpaces sends its regards), Anton, by his own account, created a first working version of the Open VSCode Server in 4 days, which was released in September 2021. Not a month later, Microsoft also made its own server solution freely available, but with a few restrictions, among other things, regarding the extensions that can be obtained from Gitpod via the Open VSX Registry portal, since the Microsoft Extension Marketplace is reserved for Microsoft’s own products. The name and the technology behind itThe name gitpod already tells us something about the technology behind the service. ‘git’ stands for the file management system behind the editor and ‘pod’ for the server technology under the editor. git…Nowadays, source code is (hopefully) no longer just stored locally on a hard disk, but centrally and managed by source code management software. The current gold standard in this area is the free Git initiated by Linus Torvalds in 2005, a distributed version management that forms the basis of popular developer platforms such as GitHub, GitLab or Bitbucket. …podA short step back in time: It’s not so long ago that IT’ers called an environment on which they published a server application machines, because it first needed hardware with processor, main memory, hard disks and an operating system of choice, which was then installed and on that in turn the said application. However, if the server application was only used sporadically, the expensive hardware was underutilized, i.e. resources were wasted. To solve this problem, work began in the 1970s on virtualizing hardware so that several logically separate systems could share the hardware to increase its utilization. Any operating system and the required server applications can be installed and operated in each of the virtual machines (VM’s) created in this way. In this concept, however, there are redundancies in the form of the operating system. If, for example, you want to run 5 Linux-based web servers, you will also have to loop along 5 possibly identical Ubuntu installations that want to be maintained. Docker is not the only, but most well-known software that addressed this problem in 2013 by abstracting another layer, in this case the operating system. Here we are now no longer talking about a machine or VM, but a container in which the server application runs. For the orchestration of these containers, the software Kubernetes initiated by Google has been available since 2015, in which the smallest deployable unit is called a pod, which contains one or more containers that share the allocated resources. Such a pod with a ready installed and configured Open VSCode Server is launched on Google Cloud Platform when a user opens a Gidpod Workspace. Let’s go … Starting a Gitpod WorkspaceIn Gitpod, you launch a project hosted on Github, Gitlab or Bitbucket into a so-called workspace. To do this, simply prepend the url of your repository with the string gitpod.io/#. Those who prefer buttons, can either use a provided bookmarklet and resort to a browser extension that adds a Gitpod button to the Github interface. Once the workspace has been started and you have closed the browser window, it will be available for a while on the dashboard under Workspaces. If you work frequently with the same project, it is advisable to create a permanent shortcut there under Projects if you are not into manual url changes or buttons. Last but not least, the icing on the cake is to install one of these Gitpod projects as a Chrome App and drop it in the toolbar. That way, the code is just a click away and the whole thing almost feels like a local VS Code due to the reduced browser window. Configuration of the workspace of a projectIn addition to the general setting options, Gitpod offers an individual configuration per project. For this the file .gitpod.yml is searched in the root of the project folder and used. All setting options are well documented in the gitpod Docs. Using a custom containerThe default container that Gitpod boots up when starting a workspace is based on Debian/Ubuntu and already includes a lot of frameworks and languages like Node, Java, Go, Python and some more. However, if you want to use a different image, you can set this via the image entry, either by referencing a public image or by specifying the name of a Dockerfile in the project. The possibilities here are numerous and can be found in the section Custom Docker Image. Tasks at startupTo get a project running in Visual Studio Code, especially in the Node environment, there are a few things that need to be set up, such as installing the correct Node.js version and dependent packages using NPM or another package manager. The same is true for Gitpod, of course, although these actions need to be done again and again after the working environment has been started, for example, if the pod has been discarded after a while. For these recurring tasks, the software provides the Tasks section in the .gitpod.yml and there, in the forefront, the init entry. In the following example, Node 14.17, all local packages and one global package are installed as a multi-line task: 12345tasks: - init: | nvm install 14.17.2 npm install npm install -g grunt-cli With the grouping and naming of tasks, the terminal display settings and the total of three execution levels before, init and command it is easy to create a configuration that starts up the workspace in a fixed way while keeping an overview. If you like it even a bit faster, you can use so-called Prebuilds, which serve as a snapshot for creating a new workspace. These prebuilds use the .gitpod.yml of the project and are closely linked to the source code management used (currently GitHub, GitLab and Bitbucket). Thus, a prebuild is recreated each time modified code is checked into the project. It doesn’t get much faster and more convenient to code from anywhere in the browser. Include extensions automaticallyWhen creating the gitpod workspace, the configuration file .gitpod.yml can also be used to include the Visual Studio code extensions that are needed for working. The easiest way to do this is if the extension is represented on the open platform Open VSX Registry, because gitpod looks for the pattern ${publisher}.${name} there by default. Example: 1234vscode: extensions: - HookyQR.beautify - kamikillerto.vscode-colorize However, VSIX files from other sources can also be included via the full url. Microsoft offers with the Visual Studio Marketplace the primary and largest source of extensions, but unfortunately omits the specification of a complete download path of the VSIX file. However, it is very easy to build this using the following pattern: https://${publisher}.gallery.vsassets.io/_apis/public/gallery/publisher/${publisher}/extension/${extension}/${version}/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage The necessary information about the variables Publisher, Extension and Version in this pattern can be obtained from the details page of an extension in Visual Studio Code. Output: 123456Name: vscode-hexo-utilsId: fantasy.vscode-hexo-utilsDescription: vscode extension for hexoVersion: 0.2.1Publisher: fantasyVS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=fantasy.vscode-hexo-utils This will result in the following entry in .gitpod.yml: 123vscode: extensions: - https://fantasy.gallery.vsassets.io/_apis/public/gallery/publisher/fantasy/extension/vscode-hexo-utils/0.2.1/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage Synchronize settingsEvery IDE looks different, depending on the tastes and preferences of the developer sitting in front of it, and so relatively quickly after the first release of VS Code, there were the first extensions that could synchronize the IDE’s settings across multiple machines (mostly via Gists), until Microsoft took on the feature and integrated it directly into the IDE. Now, with Gitpod, we have a new but slightly different instance of Visual Studio Code, but the Kielers thought of that, too. They couldn’t access the data of the integrated synchronization, but they created a by-pass with their own extension that works just as well. After the installation in VS Code and a restart of the same, one logs in to the extension with the same account under which one has registered with Gitpod and can thereafter synchronize tasks, code snippets extensions and keyboard shortcuts in addition to the settings. Prices and ServicesGitpod is a company and has to look like all others, how it can work cost-covering. Therefore, as is often the case, there are different plans that can be booked. There are four of them at Gidpod in the Saas area, whereby Free differs from the other plans only in the possible computing time, the timeout and the parallel running workspaces. Thus, in the Free plan, there are 50 hours that you can work with 4 WorkSpaces and in case of inactivity, the pods are shut down again after 30 minutes. A fair free offer for all those who want to code on the go, but most of the time have a computer and a locally installed VS Code. You can also host Gitpod yourself on an existing managed Kubernetes environment, for example if you are already a customer of cloud service providers like Amazon, Azure or Google. ConclusionIt will be exciting to see to what extent Microsoft’s own GitHub CodeSpaces will be able to hold its own against this really very manageable opponent once it has been launched en masse. The team from Kiel has at least already set the bar very high, also because it is fantastically documented and structured. A real gain for every tool collection.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Remote","slug":"Remote","permalink":"https://kiko.io/tags/Remote/"}]},{"title":"Discoveries #19 - Visual Helpers","subtitle":null,"series":"Discoveries","date":"2022-07-09","updated":"2022-07-09","path":"post/Discoveries-19-Visual-Helpers/","permalink":"https://kiko.io/post/Discoveries-19-Visual-Helpers/","excerpt":"Colors and images are the visual meat on the boil of any web solution. If you don’t convince the visitor’s eye, they will quickly leave and if users have to work with a visually poor solution, they will be too dissatisfied, no matter how well the algorithms work. Below are a few JavaScript libraries that help to create appealing interfaces. Color ThiefVibrant ColorsColor.jsTinyColorQix colorAlpha PaintletDOM to ImageimagesLoadedGraphery SVGFlickity","keywords":"colors images visual meat boil web solution dont convince visitors eye quickly leave users work visually poor dissatisfied matter algorithms javascript libraries create appealing interfaces color thiefvibrant colorscolorjstinycolorqix coloralpha paintletdom imageimagesloadedgraphery svgflickity","text":"Colors and images are the visual meat on the boil of any web solution. If you don’t convince the visitor’s eye, they will quickly leave and if users have to work with a visually poor solution, they will be too dissatisfied, no matter how well the algorithms work. Below are a few JavaScript libraries that help to create appealing interfaces. Color ThiefVibrant ColorsColor.jsTinyColorQix colorAlpha PaintletDOM to ImageimagesLoadedGraphery SVGFlickity Color Thief by Lokesh Dhakar https://lokeshdhakar.com/projects/color-thief/ Lokesh has developed a JS library which extracts a color palette from any given image. Very useful to adjust the colors of a page for example to the hero image. It works in the client as well as in Node.JS applications. Vibrant Colors by Corbin Crutchley et al https://github.com/Vibrant-Colors/node-vibrant Corbin Crutchley is one of the maintainer of the library Color Vibrant, which extracts the colors from a given image as Color Thief does, but with many more features. It classifies the colors in the extracted palette for using as common shortcuts, it has a WebWorker for avoiding freezing the UI thread and it has converting methods into several color spaces. Stunning work … see the Pen from Konstantin Polunin. Color.js - Let's get serious about color by Lea Verou & Chris Lilley https://colorjs.io/ As Lea Verou says in her blog post on releasing Color.js, there was a lack of color libraries that did the things she (and many others) needed on working with colors. So she teamed up with Chris Lilley, the father of SVG, to create a JS library that covers pretty much everything regarding color coding. I bet Color.js will become a new standard lib for all of us. TinyColor by Brian Grinstead https://github.com/bgrins/TinyColor Brian’s ambitions were certainly not the same as Lea Verou’s, but with TinyColors he has started something, that can be quite helpful on a smaller scale in converting from one color space to another. Qix color by Josh Junon https://github.com/Qix-/color Josh Junon, or ‘Qix’ on Github, provides us a lib with only 496 lines and 10.9 KB, for immutable color conversion and manipulation with support for CSS color strings. For in between… Alpha Paintlet by Dave Rupert https://daverupert.com/2021/10/alpha-paintlet/ The Web API CSS.paintWorklet (see MDN) is an experimental feature in Chromium browsers for extending CSS with JavaScript by writing Worklets. Dave shows us how to do this with his ‘Alpha Paintlet’, which manipulates the alpha channel. DOM to Image by Anatolii Saienko https://github.com/tsayen/dom-to-image Ever wanted to store an arbitary DOM node as an image? With Anatolii’s solution a breeze. Just load the library and call domtoimage.toPng(node). It supports PNG, JPEG and SVG. imagesLoaded by David DeSandro https://imagesloaded.desandro.com/ Sometimes it is important to know when an image was loaded on a website, for example to follow up with further actions. David has a Vanilla script and jQuery solution for this problem and it works with background images too. An important helper … well done. Graphery SVG by -unknown- https://www.graphery.org/svg/ Writing an SVG is not really an amusement. If you are more familiar with JS, you can use Vanilla JS with lots of createElement and setAttribut or the wrapper solution from Graphery, which is chainable and very well documented. Flickity by Evan S https://codepen.io/Skoulix/pen/BRJRPd Last but not least, a very cool hero image solution from Evan. It uses the parallax effect for sliding hero images in the background. Very cool.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Old Sweetheart Rediscovered","subtitle":"OutlookSignature lives on as Go application","date":"2022-06-21","updated":"2022-06-21","path":"post/Old-Sweetheart-Rediscovered/","permalink":"https://kiko.io/post/Old-Sweetheart-Rediscovered/","excerpt":"It seems like ages ago that I wrote a tool called OutlookSignature with Visual Basic 6 and put it on the web on my old German blog zerbit.de. But the WayBackMachine says something different. Started in 2006, I released the last version 1.9 at the beginning of December 2008. Just 14 years ago… The thing was a command line tool, that could be used to automatically generate signatures for Microsoft Outlook in the three formats TXT, RTF and HTML, for example centrally via a Windows login script for the entire organization. No hassle anymore for the users on creating an appropriate mail signature and no more stress for the marketing department in enforcing a uniform appearance. It was based on templates with placeholders for the data and configurable via an INI file. The data could come either from the ActiveDirectory via LDAP or from any database.","keywords":"ages ago wrote tool called outlooksignature visual basic put web german blog zerbitde waybackmachine started released version beginning december years ago… thing command line automatically generate signatures microsoft outlook formats txt rtf html centrally windows login script entire organization hassle anymore users creating mail signature stress marketing department enforcing uniform appearance based templates placeholders data configurable ini file activedirectory ldap database","text":"It seems like ages ago that I wrote a tool called OutlookSignature with Visual Basic 6 and put it on the web on my old German blog zerbit.de. But the WayBackMachine says something different. Started in 2006, I released the last version 1.9 at the beginning of December 2008. Just 14 years ago… The thing was a command line tool, that could be used to automatically generate signatures for Microsoft Outlook in the three formats TXT, RTF and HTML, for example centrally via a Windows login script for the entire organization. No hassle anymore for the users on creating an appropriate mail signature and no more stress for the marketing department in enforcing a uniform appearance. It was based on templates with placeholders for the data and configurable via an INI file. The data could come either from the ActiveDirectory via LDAP or from any database. I had quite a large fan base and even years later I kept getting requests to integrate new features. The problem I faced after releasing the last version 1.9 in 2008 was on the one hand that Microsoft had fundamentally changed the handling of signatures in Outlook and on the other hand that VB6 was no longer really en-vogue, because everybody (and me too) was switching to VB.NET or C# and that would have meant a complete re-write for me. But there were already commercial alternatives to OutlookSignature at that time and I had turned to other projects, but it was available into 2012 until I decided to close my personal blog. However, to this day there are one or two websites where you can download or find information about it. OutlookSignature was always freeware, but never Open Source, because of the fact, that I used some code, that did not come from me and was not approved for publication. Accidental Discovery of a Go-based ImplementationI own the domain zerbit.de until today and the other day I was looking on the web for some references to it and stumbled across a GitHub page with the title An open source reimplementation of Kristof Zerbe’s (ZerbIT) “OutlookSignature” … What The Heck!? I don’t know who the user ‘foobar0815’ is, but he/she is definitely German and had the patience to rebuild my tool in the modern language Go in 2019. GoSignature uses the orginal INI configuation file, with the field mappings and all the other stuff, but the database connection feature. It works only with LDAP, which is quite enough today. I have no idea about Go, but I will certainly look into it a bit in the near future and find out if this thing works. If you have already tried GoSignature or even if you are the author, please contact me. Would be fun to talk about it… :)","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Mail","slug":"Mail","permalink":"https://kiko.io/tags/Mail/"},{"name":"Office","slug":"Office","permalink":"https://kiko.io/tags/Office/"}]},{"title":"Simplest Console File Logger","subtitle":"How to implement your own logger with just a StringBuilder in C#","date":"2022-06-19","updated":"2022-06-19","path":"post/Simplest-Console-File-Logger/","permalink":"https://kiko.io/post/Simplest-Console-File-Logger/","excerpt":"When you need to do a task in IT and don’t need a fancy user interface, you usually turn to a console application or develop one. But no UI means not, that you don’t want to get some information about the status of the running program. The means of choice in C#/.NET is then the output of certain values in the console via console.WriteLine(). But often these applications are supposed to run in the background or hidden, so that crashes or errors are not immediately noticeable if you don’t have at least a rudimentary logging built in. Such logging is also useful for the later evaluation of program runs. For this reason pretty much all programs log in some way. Currently there an incredible number of logging frameworks available, that make it to the news every now and then, like Log4Shell for Java. The best known in the .NET area is Microsoft’s own ILogger (Microsoft.Extensions.Logging), log4net, NLog and Serilog. What they all have in common is, that they are highly flexible in terms of configuration, usage and storage of the logs. On the other hand, they are fat beasts, you have to learn to handle first and you have to deliver with the program always. The latter does not apply to the ILogger, but Microsoft has been known to complicate things so unnecessarily that it is almost no fun anymore. Many times this overload is simply not necessary, when you just need a log file for each run of your console application. Let me show you, how to achieve this with a dead simple StringBuilder…","keywords":"task dont fancy user interface turn console application develop ui means information status running program choice c#/net output values consolewriteline applications supposed run background hidden crashes errors immediately noticeable rudimentary logging built evaluation runs reason pretty programs log incredible number frameworks make news log4shell java net area microsofts ilogger microsoftextensionslogging log4net nlog serilog common highly flexible terms configuration usage storage logs hand fat beasts learn handle deliver apply microsoft complicate things unnecessarily fun anymore times overload simply file show achieve dead simple stringbuilder…","text":"When you need to do a task in IT and don’t need a fancy user interface, you usually turn to a console application or develop one. But no UI means not, that you don’t want to get some information about the status of the running program. The means of choice in C#/.NET is then the output of certain values in the console via console.WriteLine(). But often these applications are supposed to run in the background or hidden, so that crashes or errors are not immediately noticeable if you don’t have at least a rudimentary logging built in. Such logging is also useful for the later evaluation of program runs. For this reason pretty much all programs log in some way. Currently there an incredible number of logging frameworks available, that make it to the news every now and then, like Log4Shell for Java. The best known in the .NET area is Microsoft’s own ILogger (Microsoft.Extensions.Logging), log4net, NLog and Serilog. What they all have in common is, that they are highly flexible in terms of configuration, usage and storage of the logs. On the other hand, they are fat beasts, you have to learn to handle first and you have to deliver with the program always. The latter does not apply to the ILogger, but Microsoft has been known to complicate things so unnecessarily that it is almost no fun anymore. Many times this overload is simply not necessary, when you just need a log file for each run of your console application. Let me show you, how to achieve this with a dead simple StringBuilder… Lets start in the Program.cs of a new .NET Core 6 console application. As you may know, in this version MS has changed the entry point static void Main(string[] args), which has existed for what feels like centuries, to the so-called Top-Level Statements, which means nothing else, that the entry point is now automatically generated … and what most old-school developer think sucks, because you have little control over it anymore. But for this approach we need a global variable and therefore I’ve recreated Main like this: Program.cs123456789101112// TOP-LEVEL STATEMENTSMain.Start(args);// MAIN MODULEstatic public class Main { static public void Start(string[] args) { //... }} Preparation of a small senseless console applicationLet’s implement a little bit of the ‘features‘ first, like a new class called DoSomething that does the actual job: DoSomething.cs123456789101112public class DoSomething{ public void Work(string param1) { //... doing heavy work with param1 } public void MoreWork(string param2) { //... doing more work with param2 }} Now let’s process the command line arguments for running the app called SimplestConsoleFileLogger like this… SimplestConsoleFileLogger.exe param1:this param2:that First we create a new class for holding the parameters… Parameters.cs12345public class Parameter{ public string Param1 = ""; public string Param2 = "";} … and then we populate a new instance of it with the values from the console arguments and run the worker class: Program.cs1234567891011121314151617181920212223static public class Main { static Parameter PARAMETER = new Parameter(); static public void Start(string[] args) { //Process Arguments foreach (string arg in args) { var a = arg.Split(':'); switch (a[0].ToUpper()) { case "PARAM1": PARAMETER.Param1 = a[1]; break; case "PARAM2": PARAMETER.Param1 = a[1]; break; default: break; } } DoSomething ds = new DoSomething(); ds.Work(PARAMETER.Param1); ds.MoreWork(PARAMETER.Param2); }} The LoggingNow we have our pretty senseless app and we want to log the jobs of the worker class. First, we have to implement a global variable for holding the log messages and secondly a small method to simplify the handling of the log: Program.cs12345678910111213static public class Main { ... static StringBuilder LOGDATA = new StringBuilder(); public static void Log(params string[] args) { var msg = string.Join(" ", args).TrimEnd(); LOGDATA.AppendLine(msg); } } The Log methods gets an infinite number of arguments (ParamArray) to assemble a message to log. This we can use now in our worker class: DoSomething.cs1234567891011121314public class DoSomething{ public void Work(string param1) { //... doing heavy work with param1 Main.Log("Heavy work done with result", "SUCCESS"); } public void MoreWork(string param2) { //... doing more work with param2 Main.Log("More work done with result", "SUCCESS"); }} You can use Main.Log(...) wherever you want, because its globally available. Writing the log fileLast, but not least, we have to persist the log messages into a file. This should done just before the application ends. For writing the log, we use a StreamWriter object, which accepts our StringBuilder, where the messages are stored in seperate lines. By checking the length of the StringBuilder LOGDATA, we ensure that the log file is only written, when there is something to write. Program.cs123456789101112131415161718192021static public class Main { static public void Start(string[] args) { ... HandleEnd(); } static void HandleEnd() { if (LOGDATA.Length > 0) { string logFile = $"{DateTime.Now.ToString("u").Replace(":","-")}.log"; string logFilePath = Path.Combine(Environment.CurrentDirectory, logFile); StreamWriter logStream = new StreamWriter(logFilePath, true, Encoding.UTF8); logStream.Write(LOGDATA); logStream.Close(); } }} That’s it. As you see, the important parts are the globally available StringBuilder and the persistance of the messages in a file at the end. With this you can customize the messages as fancy as you want and you don’t need one of the logging beasts. Download the files via Gist","categories":[{"name":".NET","slug":"NET","permalink":"https://kiko.io/categories/NET/"}],"tags":[{"name":"Visual Studio","slug":"Visual-Studio","permalink":"https://kiko.io/tags/Visual-Studio/"},{"name":"Logging","slug":"Logging","permalink":"https://kiko.io/tags/Logging/"},{"name":"C#","slug":"C","permalink":"https://kiko.io/tags/C/"}]},{"title":"Thanks Dropbox, but I'm off","subtitle":"How to do homework or say goodbye to the market","date":"2022-05-13","updated":"2022-05-13","path":"post/Thanks-Dropbox-but-I-m-off/","permalink":"https://kiko.io/post/Thanks-Dropbox-but-I-m-off/","excerpt":"I’m a customer of Dropbox many, many years, a paying customer, and was always happy about the service, as it is fast and easy to use. No problems … until now! Be aware … this will be a rant :/ But first a step back: Nowadays it is normal to work with different classes of devices: stationary PCs, different types of laptops, tablets and also smartphones. You start writing a text on your laptop at home in the garden, have to interrupt it because of an appointment and continue writing on your tablet on your way, only to finish it at home on your PC, because it has started to rain. All of these used devices to write the text may have a different hard- and software configuration, but today we have synchronisation services like Dropbox, OneDrive, GoogleDrive and man others, which ensures that the same version of the text is available at all times at all devices. To me it is obvious that software manufacturers cannot support every operating system, but I can expect that if they support a particular OS, they will do so on any hardware that the OS manufacturer also supports!","keywords":"im customer dropbox years paying happy service fast easy problems … aware rant / step back nowadays normal work classes devices stationary pcs types laptops tablets smartphones start writing text laptop home garden interrupt appointment continue tablet finish pc started rain write hard- software configuration today synchronisation services onedrive googledrive man ensures version times obvious manufacturers support operating system expect os hardware manufacturer supports","text":"I’m a customer of Dropbox many, many years, a paying customer, and was always happy about the service, as it is fast and easy to use. No problems … until now! Be aware … this will be a rant :/ But first a step back: Nowadays it is normal to work with different classes of devices: stationary PCs, different types of laptops, tablets and also smartphones. You start writing a text on your laptop at home in the garden, have to interrupt it because of an appointment and continue writing on your tablet on your way, only to finish it at home on your PC, because it has started to rain. All of these used devices to write the text may have a different hard- and software configuration, but today we have synchronisation services like Dropbox, OneDrive, GoogleDrive and man others, which ensures that the same version of the text is available at all times at all devices. To me it is obvious that software manufacturers cannot support every operating system, but I can expect that if they support a particular OS, they will do so on any hardware that the OS manufacturer also supports! What has happened…I have similar hardware as described above, but with a gap: a Windows tablet. I have one with Android and manage my synchronization with a tool called DropSync, but due to limited disc space I can’t sync bigger projects with thousands of files, like this blog. Whenever I want to continue writing an article on the road, I download the corresponding MD file and then upload it again afterwards. A bit cumbersome. Since the first appearance of Microsoft’s Surface several years ago, I thought this type of hardware, which includes the same OS I’m using all the time - Windows - could be close my device gap and a couple of days ago, I put a bunch of Euros in the hand of a local dealer to finally close it with a Surface Pro X. 256GB SSD, 16GB RAM and the size of a sheet of paper should be good to work on stuff while on the road. As with any new piece of hardware, I began to install my setup, essential tools that I use very often. This included Dropbox, of course … with an unexpected result. While all the tools installed without a murmur, Dropbox bitched by saying that my device is not compatible with this version of the Dropbox client and that I should install the Dropbox S Mode app via the Windows Store instead. Yes sure, a Surface has an ARM processor instead of an x32/x64 from Intel or AMD, but what’s the problem? Every single tool in my list works with Windows 10 under ARM64, at least in 32-bit emulation, but not Dropbox? To say it loud and clear: the UWP app “Dropbox S Mode” is a disgrace! Not only does it use a technology that Microsoft is just saying goodbye to, no, it also offers less functionality than the actual Dropbox website. Folders cannot be downloaded as a whole, but only individual files, and a synchronization of some kind has been completely omitted. The thing is really just dirt. This kind of software is for grannies who already have a hard time holding a mouse, but not for power users, with several gigabytes of data! I don’t let something like that sit on me so easily, but tried to find a trick via some research how to get the Dropbox client to work under ARM after all. And on Dropbox’s own forum alone, I found a bunch of posts on the topic: Is Windows 10 on ARM going to be compatible with the Dropbox desktop app? Can I have the desktop app installed on my Surface Pro X? Dropbox client for Windows 11 ARM? Can’t install the desktop app on a new PC The oldest post I have found here was from 2019. Dropbox, you are not able to port your code to ARM within at least 3 years and the only answer you have for your paying users is buy a regular computer. Are you nuts? Are you begging to go under or be bought by one of the big fish due to management incompetence? New Dropbox features like Paper, Capture or Replay are nice ones, but whenever a company neglects its base, the reason why became big, it is close to the abyss. Best examples are Lego and Marvel. A New StartI was aware of OneDrive (formerly SkyDrive) since it was launched 2007, as Microsoft tried to counter the success of Dropbox, but it had its pitfalls and wasn’t running so fluffy as the original. 15 years later, you can hardly resist the sync software from Redmont, because it is always included in everything that has Windows on it, but it has also become decisively better and more stable. The ARM disaster of Dropbox has now moved me to switch to OneDrive. I’m writing this article on my Surface tablet right now, and whenever I press CTRL-S, OneDrive syncs the MD file to my other machines. At the end of the day, we’re all about one thing: Convenience, and if you don’t meet that standard (or abuse it), you die. Good Luck Dropbox …","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Rant","slug":"Rant","permalink":"https://kiko.io/tags/Rant/"}]},{"title":"Discoveries #18 - JS UI","subtitle":null,"series":"Discoveries","date":"2022-05-10","updated":"2022-05-10","path":"post/Discoveries-18-JS-UI/","permalink":"https://kiko.io/post/Discoveries-18-JS-UI/","excerpt":"This months Discoveries it’s all about Web UI driven by JavaScript. I have found some really cool components in the net you can use in your project right away to give your users a bit more food for the eye. Congrats to the developers for their amazing work. Scroll-Linked Animations With the Web Animations API (WAAPI) and ScrollTimelineSlider ScrollAdd-to-Calendar ButtonResponsive Clock UiSwiffy SliderFloating UI8 CSS & JavaScript Snippets for Creating Cool Card UI Hover EffectsHow To Build An Expandable Accessible GalleryOmicron drag&dropLetMeScroll","keywords":"months discoveries web ui driven javascript found cool components net project give users bit food eye congrats developers amazing work scroll-linked animations api waapi scrolltimelineslider scrolladd-to-calendar buttonresponsive clock uiswiffy sliderfloating ui8 css snippets creating card hover effectshow build expandable accessible galleryomicron drag&dropletmescroll","text":"This months Discoveries it’s all about Web UI driven by JavaScript. I have found some really cool components in the net you can use in your project right away to give your users a bit more food for the eye. Congrats to the developers for their amazing work. Scroll-Linked Animations With the Web Animations API (WAAPI) and ScrollTimelineSlider ScrollAdd-to-Calendar ButtonResponsive Clock UiSwiffy SliderFloating UI8 CSS & JavaScript Snippets for Creating Cool Card UI Hover EffectsHow To Build An Expandable Accessible GalleryOmicron drag&dropLetMeScroll Scroll-Linked Animations With the Web Animations API (WAAPI) and ScrollTimeline by Bramus Van Damme https://css-tricks.com/scroll-linked-animations-with-the-web-animations-api-waapi-and-scrolltimeline/ Bramus shows us how to implement a reading progress bar with the new ScrollTimeline feature coming up within the next versions of Chrome and other browsers, including a polyfill from Robert Flack. As you read this … look at the orange bar at the top. Slider Scroll by Edixon Piña https://sliderscroll.netlify.app/ Ever wanted to create a web application consisting of individual slides? Then, this is for you. Edixon has to (nearly) perfect and clean solution for this on GitHub. Add-to-Calendar Button by Jens Kuerschner https://github.com/jekuer/add-to-calendar-button Jens offers a nice solution for everybody who wants their users to add an events to their calendar within two clicks. It is based on inline JSON-snippets and has several features like dynmamic days, timezone offsets, schema.org support and not to forget, a pleasing UI. Responsive Clock Ui by Bedimcode https://github.com/bedimcode/responsive-clock-ui Marlon, a web designer from Peru, who is known on the web as Bedimcode, has created a stunning CSS- and JS-driven analog clock, with a light and a dark theme. Maybe a bit useless for a website, but wonderfully crafted and nice to look at. You can find a demo here. Swiffy Slider by DynamicWeb https://swiffyslider.com/ Sliders and carousels are almost everywhere and there are many implementations out there, but this one from the team of DynamicWeb is a bit outstanding because of its features and application possibilities. Well done … espacially the well documentation. No questions left. Floating UI by Unknown https://floating-ui.com/ Whoever has created this solution, is hardly anything to add to their claim: “Floating UI is a low-level toolkit to create floating elements. Tooltips, popovers, dropdowns, menus, and more.”. and it is hardly anything to add to their feature list. Whenever something needs to float … use this library. 8 CSS & JavaScript Snippets for Creating Cool Card UI Hover Effects by Eric Karkovack https://speckyboy.com/css-javascript-card-ui-hover-effects/ Cards are a good way to bring order in a UI. How to make such cards exciting shows Eric with his collection of stunning card designs. Personally I’m in love with the ‘Profile Card Hover Effect’… How To Build An Expandable Accessible Gallery by Silvestar Bistrović https://www.smashingmagazine.com/2021/10/build-expandable-accessible-gallery/ Photo in galleries or other grid based collections should have the possibility to expand an item in order to see either the full photo or some details of the selected item. To avoid calling a detail page or something similar, Silvestar has developed a method to display this animated in fullscreen mode. Can’t wait to find a problem to check this out. Omicron drag&drop by Franek Boehlke https://github.com/mcFrax/omicron-dnd What sounds like a variant of the Corona virus is a fast JavaScript drag-and-drop library for desktop and mobile browsers, made by Franek Boehlke. And this is no coincidence, but rather a pandemic project. It is lean, but has all basic features to nudge things around on your web app. LetMeScroll by Bruno Vieira https://bmsvieira.github.io/letmescroll.js/demo/index.html Quite a while ago a have implemented a custom scrollbar on my own in a Web App and it wasn’t so funny as I thought. Lot of things to care about. If Bruno had already finished his project at that time, I could have saved myself the work. He brings a native scroll behaviour for desktop and mobile, easy customization and multiple callbacks in 465 lines of vanilla JS code. Great job, Bruno.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Discoveries #17 - CSS","subtitle":null,"series":"Discoveries","date":"2022-03-11","updated":"2022-03-11","path":"post/Discoveries-17-CSS/","permalink":"https://kiko.io/post/Discoveries-17-CSS/","excerpt":"Easing Functions Cheat SheetA New Way To Reduce Font Loading Impact: CSS Font DescriptorsContainer Queries in Web ComponentsMulti Color Text With CSSConditional Border Radius In CSSIcons in Pure CSSparallax.cssDefensive CSSHow to Create a CSS-Only Loader Using One ElementGetting Your head Straight: A New CSS Performance Diagnostics Snippet","keywords":"easing functions cheat sheeta reduce font loading impact css descriptorscontainer queries web componentsmulti color text cssconditional border radius cssicons pure cssparallaxcssdefensive csshow create css-only loader elementgetting head straight performance diagnostics snippet","text":"Easing Functions Cheat SheetA New Way To Reduce Font Loading Impact: CSS Font DescriptorsContainer Queries in Web ComponentsMulti Color Text With CSSConditional Border Radius In CSSIcons in Pure CSSparallax.cssDefensive CSSHow to Create a CSS-Only Loader Using One ElementGetting Your head Straight: A New CSS Performance Diagnostics Snippet Easing Functions Cheat Sheet by Andrey Sitnik & Ivan Solovev https://github.com/ai/easings.net Ever wanted to implement an easing function in CSS and dont know how? Andrey and Ivan provide a really useful web project via Github, with the code of 30 easing function, including samples and the math functions behind. Download or fork and run locally or simply use this gitpod Link. A New Way To Reduce Font Loading Impact: CSS Font Descriptors by Barry Pollard https://www.smashingmagazine.com/2021/05/reduce-font-loading-impact-css-descriptors/ Barry gives an insight into the modern use of web fonts, hints on how to use font-display and an outlook on how everything could be better in the future. Container Queries in Web Components by Max Böck https://mxb.dev/blog/container-queries-web-components/ Container Queries are one of the most anticipated new features in CSS and Max gives an insight into the possibilities, including a demo. Multi Color Text With CSS by Shireen Taj https://codepen.io/TajShireen/full/YzZmbep UI developer Shireen has made a wonderful sample on CodePen on how to show text in multile colors using linear gradient backgrounds. Conditional Border Radius In CSS by Ahmad Shaheed https://ishadeed.com/article/conditional-border-radius/ CSS expert Ahmad shows us a smart way to provide different border radius regarding the size of an object in the viewport, he has learned from the facebook developers. Icons in Pure CSS by Anthony Fu https://antfu.me/posts/icons-in-pure-css Anthony has found a clever way on combining the icon framework iconify with CSS, to save the use of icon fonts and deliver pure SVG instead. parallax.css by Matyanson https://github.com/Matyanson/parallax.css/ Github user ‘Matyanson’ has written a CSS package for using parallax effects easily on every website. Defensive CSS by Ahmad Shaheed https://ishadeed.com/article/defensive-css/ In order to prevent pittfalls, you have to use CSS wisely. Ahmad show us some techniques on how to write defensive and thus protected code. How to Create a CSS-Only Loader Using One Element by Temani Afif https://www.freecodecamp.org/news/how-to-create-a-css-only-loader Loaders are everywhere and many of them are based on images or SVG. But this does not have to be, as Temani finds. CSS is perfectly sufficient for this. Getting Your head Straight: A New CSS Performance Diagnostics Snippet by Vitaly Friedman https://www.smashingmagazine.com/2021/09/css-head-tag/ Chrome Debugger has tons of possibilities to ensure to find performance bottlenecks or other discrepancies. Vitaly brings us closer a little snippet that Harry Roberts wrote, which exposes potential performance issues in your page’s <head> tag: ct.css.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Checker Plus - Gmail in better...","subtitle":"Few Chrome browser extensions are as close to user needs as Checker Plus for Google's Gmail, Calendar and Drive services.","series":"Golem","date":"2022-02-12","updated":"2022-02-12","path":"post/Checker-Plus-Gmail-in-better/","permalink":"https://kiko.io/post/Checker-Plus-Gmail-in-better/","excerpt":"Those who have a Google account usually also use the web-based mailer Gmail, often Google Calendar for appointments and perhaps Google Drive for storing files in the cloud. The US internet company makes it very easy for users to use its web-based services. The entry barriers are low and the interfaces are tidy and intuitive. If you use the in-house Chrome browser, you can also be informed about incoming mails or upcoming appointments. One click and the browser opens with Gmail or Google Calendar … But it is not always necessary or desirable to load one of the Google tools completely in a new browser window, when you may already have two or three dozen tabs open in several windows and are in danger of losing track of them. That’s what Montreal-based software developer Jason Savard seems to have thought ten years ago. At that time, he began writing a Chrome extension that would report a newly received Gmail by means of a small icon in the extension bar of the browser and display it directly in a separate pop-up window by clicking on it. The result was Checker Plus for Gmail.","keywords":"google account web-based mailer gmail calendar appointments drive storing files cloud internet company makes easy users services entry barriers low interfaces tidy intuitive in-house chrome browser informed incoming mails upcoming click opens … desirable load tools completely window dozen tabs open windows danger losing track montreal-based software developer jason savard thought ten years ago time began writing extension report newly received means small icon bar display directly separate pop-up clicking result checker","text":"Those who have a Google account usually also use the web-based mailer Gmail, often Google Calendar for appointments and perhaps Google Drive for storing files in the cloud. The US internet company makes it very easy for users to use its web-based services. The entry barriers are low and the interfaces are tidy and intuitive. If you use the in-house Chrome browser, you can also be informed about incoming mails or upcoming appointments. One click and the browser opens with Gmail or Google Calendar … But it is not always necessary or desirable to load one of the Google tools completely in a new browser window, when you may already have two or three dozen tabs open in several windows and are in danger of losing track of them. That’s what Montreal-based software developer Jason Savard seems to have thought ten years ago. At that time, he began writing a Chrome extension that would report a newly received Gmail by means of a small icon in the extension bar of the browser and display it directly in a separate pop-up window by clicking on it. The result was Checker Plus for Gmail. In addition to the extension for Gmail, Savard offers others for Calendar and Drive and all of them have one thing in common: You can do the most important functions of the services directly in the popup that opens: Reply to mail, write new mail, create appointment with reminder, upload file, get file link to share - Everything works without having to load the services’ interfaces in a browser window. For the author and probably some of the almost 2 million users, the Checker Plus extensions are the kind of tools you can’t imagine working without them - that’s how quickly you get used to the functionality and adapt your own workflow to it. Checker Plus for Gmail: Impressive range of functionsThe Checker Plus tool for the web mailer Gmail places an icon in the extension bar that displays the number of unread mails by means of an indicator. Since it is possible to configure several Gmail accounts in the extension, all unread mails are summarised in the number. If you click on the icon, a pop-up opens with a clear list of all accounts and the unread mails. The mails are displayed in compact mode, i.e. in addition to the sender and the subject, you also see an excerpt from the mail text. If you now click on a mail, it is displayed in full in the popout and also takes any mail thread into account. The icon buttons Archive, Mark Spam, Delete, Mark Read and Mark, which are displayed when hovering over a mail, represent the most frequently used actions and usually make it unnecessary to use the Open in Gmail button. Massive Setting OptionsOver the years and with each new version (currently v22.9), countless options have been added, so that Savard had to divide them into nine groups, including the start page: Start Page with Quick Access General Notifications Do not disturb Button Accounts/Labels Skins and Themes Keys Voice Input It would be foolish, to go into all the settings in this article, but a few deserve an extra portion of attention, such as those under Accounts/Labels. Here, users can decide whether the extension should determine the available Gmail accounts itself based on the login to Gmail saved in the browser or whether they should specify them manually. In both cases, it is then possible to select only certain labels for the notifications or to leave it at the standard inbox. This is very usefull if you have set up a system of automatic distribution to subfolders (labels). Since the extensions also control the interactive desktop notifications, it is useful to be able to define periods of time under Do Not Disturb, when this is not to happen. In addition to the option of using dates from the Google Calendar, you can also define weekly schedules for this purpose. Checker Plus for Google Calendar: Better than the originalThe icon of the Chrome extension for Google Calendar visually warns of upcoming appointments and displays those for the next two days in the tooltip. If you sit at your computer all day and work in the Chrome browser, you can hardly talk your way out of missing an appointment, especially since the extension also takes over warnings via the Windows notification system. The pop-up’s interface is similar to the original at calendar.google.com, merely placing the sidebar under a hamburger menu to save space. It replicates the basic functionality of the original and there is little reason to visit the original Google website when using it. But it also does a few things better than Google. For example, the pop-up offers the same view options as day, month, week, year and appointment overview, but calls the latter a list and adds an overview as known from the Calendar Android app. In addition, you can define your own view in the settings, for example, if you want to keep an eye on the next three days or six weeks. The current version v26.1.1 of Checker Plus for Google Calendar also has a lot of setting options, such as displaying the week numbers or marking the weekends. Particularly noteworthy here are the settings for notifications, which can be fine-tuned very well across multiple accounts and calendars. Checker Plus for Google Drive: Fewer functionsThe pop-up of the Drive extension is basically a copy of the Google interface at drive.google.com. It is limited to the list form and does without previews of the files, but also without advertisements, such as “Buy storage space”. It also has nowhere near as many options in the context menu of a folder or file as the original, but this can generally be gotten over because the most important options such as download, copy link, rename and remove are available. Additional functions against donationFew Chrome extensions are as close to the needs of users as the Checker Plus extensions. The functional scope of the Gmail extension in particular is simply impressive and leaves hardly anything to be desired. Only when it comes to localising the source code in currently more than 30 languages, Savard could take a little more care. Sometimes a little English gets mixed into otherwise German interfaces, especially in the settings. But any developer who has ever tried to offer his tool in so many languages will forgive him, because he knows the amount of work involved. Unfortunately, the settings area Skins and Themes is everything but generally understandable and no comparison to the otherwise good UX of the extensions. If you want to make the pop-up pretty with background images and other colours, you have to read up and understand Jason’s concept of individualising separate elements. Since Jason Savard now works full time on his extensions, he is dependent on income to be generated from them. He has declared some settings, which he says he worked on for a long time, as “additional functions” that you get if you donate an amount of money to him once or monthly. A fair concept, especially since he guarantees daily updates and even a response time of five minutes in case of problems. Well ;) But it’s nice to see a developer who puts so much heart and soul into his work.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"},{"name":"Mail","slug":"Mail","permalink":"https://kiko.io/tags/Mail/"}]},{"title":"Discoveries #16 - JavaScript","subtitle":null,"series":"Discoveries","date":"2022-01-29","updated":"2022-01-29","path":"post/Discoveries-16-JavaScript/","permalink":"https://kiko.io/post/Discoveries-16-JavaScript/","excerpt":"In the first discoveries of 2022 I would like to offer you some interesting links to JavaScript articles that have general language features as their topic or extend them in a clever way. The language (respectively the ECMA standard behind it) is growing from year to year and it is exiting to see how it is expanding. Modern Javascript: Everything you missed over the last 10 yearsHow can I define an enum in JavaScript?'export default thing' is different to 'export { thing as default }'Dynamic, Conditional Imports10 Client-side Storage Options and When to Use ThemAn Intro to JavaScript ProxyThe Observer Pattern in JavaScript - The Key to a Reactive BehaviorWhy JavaScript Developers Should Prefer Axios Over FetchToolkit for managing multiple promisesPromisify an entire object or class instance","keywords":"discoveries offer interesting links javascript articles general language features topic extend clever ecma standard growing year exiting expanding modern missed yearshow define enum javascript'export default thing export { }'dynamic conditional imports10 client-side storage options theman intro proxythe observer pattern - key reactive behaviorwhy developers prefer axios fetchtoolkit managing multiple promisespromisify entire object class instance","text":"In the first discoveries of 2022 I would like to offer you some interesting links to JavaScript articles that have general language features as their topic or extend them in a clever way. The language (respectively the ECMA standard behind it) is growing from year to year and it is exiting to see how it is expanding. Modern Javascript: Everything you missed over the last 10 yearsHow can I define an enum in JavaScript?'export default thing' is different to 'export { thing as default }'Dynamic, Conditional Imports10 Client-side Storage Options and When to Use ThemAn Intro to JavaScript ProxyThe Observer Pattern in JavaScript - The Key to a Reactive BehaviorWhy JavaScript Developers Should Prefer Axios Over FetchToolkit for managing multiple promisesPromisify an entire object or class instance Modern Javascript: Everything you missed over the last 10 years by Sandro Turriate https://turriate.com/articles/modern-javascript-everything-you-missed-over-10-years Sandra has written this post about the language features of ECMA Script 2020 a couple of months ago as a kind of CheatSheet, with runnable examples and a lot of useful background knowledge. How can I define an enum in JavaScript? by Angelos Chalaris https://www.30secondsofcode.org/articles/s/javascript-enum Angelos describes is this post, two different ways to define enums in JavaScript, as you might know them other languages. 'export default thing' is different to 'export { thing as default }' by Jake Archibald https://jakearchibald.com/2021/export-default-thing-vs-thing-as-default/ IMPORT and EXPORT are a fine way to separate code, but you need to know and keep in mind a few things, as Jake shows us here. Dynamic, Conditional Imports by Chris Coyier https://css-tricks.com/dynamic-conditional-imports/ When you separate code in different ES modules, you may come to the point where you want import a module depending on a specific condition. Chris show us here, how to deal with this easily. 10 Client-side Storage Options and When to Use Them by Craig Buckler https://www.sitepoint.com/client-side-storage-options-comparison/ Especially if you are writing a web app, you need to consider what storage options you have, to provide the user an optimal user experience. Craig list them all in his post and goes into the specific characteristics and possible uses. An Intro to JavaScript Proxy by Travis Almand https://css-tricks.com/an-intro-to-javascript-proxy/ JavaScript provides a PROXY, which enables you to intercept and redefine fundamental operations for an object. Is it not that common to use it, but it has great advantages, as Travis show us. The Observer Pattern in JavaScript - The Key to a Reactive Behavior by Fernando Doglio https://blog.bitsrc.io/the-observer-pattern-in-javascript-the-key-to-a-reactive-behavior-f28236e50e10 Sometimes it is necessary to decouple functionality in JS, in order to write cleaner code or to increase complexity. Fernando shows us how to implement our own observers, with subscribing, notifying and all that stuff. Why JavaScript Developers Should Prefer Axios Over Fetch by Sabesan Sathananthan https://betterprogramming.pub/why-javascript-developers-should-prefer-axios-over-fetch-294b28a96e2c FETCH is the method defined in the ECMA standard to get data from remote servers, but in the meanwhile the library Axios has become almost a de-facto standard in the industry and Sabesan tells why. Toolkit for managing multiple promises by Anthony Fu https://github.com/antfu/p This tiny library makes it easier to deal with multiple promises as it PROMISE.ALL does. Remarkable. Promisify an entire object or class instance by Gar https://github.com/wraithgar/gar-promisify Mixing async with non-async code may have some pitfalls, you can avoid by using Gar’s tiny library.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"CSS Columns and Drop Shadow","subtitle":"How to fix Chromes bug on showing shadows in a column (masonry) layout","date":"2022-01-15","updated":"2022-01-15","path":"post/CSS-Columns-and-Drop-Shadow/","permalink":"https://kiko.io/post/CSS-Columns-and-Drop-Shadow/","excerpt":"As of today, there is no true Masonry Layout technique in web development that can be implemented exclusively with CSS and is not based on JavaScript. True in the meaning, that the reading direction should be from left to right and not in the form of columns from top to bottom. For the latter, also called Fake Masonry, there are even two implementation options in CSS: Columns or Flex, whereas the Columns variant is the much simpler one. Lets say you have a list of boxes you want to show in a grid-like list, but the height of every box is defined by its content, which results in different heights. Here is an example with a base64 encoded 1px image with an individually defined height: 123456789101112131415<div class="wrapper"> <div class="item"> Box 1 <img style="height:53px" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /> </div> <div class="item"> Box 2 <img style="height:50px" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /> </div> <div class="item"> Box 3 <img style="height:33px" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /> </div> <!-- ... 6 more items with different heights --></div> By applying column-count and column-gap to the wrapper and a margin-bottom with the same value as the gap to each item, you will achieve this: The order of the boxes is from top to bottom and then from left to right … Fake Masonry, but is works as expected. Here is the most important CSS:","keywords":"today true masonry layout technique web development implemented exclusively css based javascript meaning reading direction left form columns top bottom called fake implementation options flex variant simpler lets list boxes show grid-like height box defined content results heights base64 encoded 1px image individually 123456789101112131415<div class="wrapper"> <div class="item"> <img style="height53px" src="dataimage/gifbase64r0lgodlhaqabaiaaap///waaach5baeaaaaalaaaaaabaaeaaaicraeaow==" /> </div> style="height50px" style="height33px" <-- items --></div> applying column-count column-gap wrapper margin-bottom gap item achieve order … works expected important","text":"As of today, there is no true Masonry Layout technique in web development that can be implemented exclusively with CSS and is not based on JavaScript. True in the meaning, that the reading direction should be from left to right and not in the form of columns from top to bottom. For the latter, also called Fake Masonry, there are even two implementation options in CSS: Columns or Flex, whereas the Columns variant is the much simpler one. Lets say you have a list of boxes you want to show in a grid-like list, but the height of every box is defined by its content, which results in different heights. Here is an example with a base64 encoded 1px image with an individually defined height: 123456789101112131415<div class="wrapper"> <div class="item"> Box 1 <img style="height:53px" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /> </div> <div class="item"> Box 2 <img style="height:50px" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /> </div> <div class="item"> Box 3 <img style="height:33px" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /> </div> <!-- ... 6 more items with different heights --></div> By applying column-count and column-gap to the wrapper and a margin-bottom with the same value as the gap to each item, you will achieve this: The order of the boxes is from top to bottom and then from left to right … Fake Masonry, but is works as expected. Here is the most important CSS: 123456789101112131415161718192021222324252627:root { --gap: 20px; --gap-half: calc(var(--gap) / 2);}.wrapper { /* LAYOUT STYLES */ width: max-content; margin: 0; padding: var(--gap-half); /* COLUMN STYLES */ column-count: 3; column-gap: var(--gap);}.item { /* BASIC STYLES */ background-color: #fffaf5; /* LAYOUT STYLES */ width: 200px; margin-bottom: var(--gap); padding: 15px; overflow: auto; /* COLUMN STYLES */ break-inside: avoid-column;} See the pen for the complete HTML and CSS: Drop Shadow and the Chromium BugTo make the list visually a little bit more interesting, we now add a shadow, which is half as thick as the gap, to the boxes: 12345678910:root { --gap: 20px; --gap-half: calc(var(--gap) / 2); --color-border: hsl(0 0% 90%);}.item { /* ... more styles */ border: 1px var(--color-border) solid; box-shadow: 1px 2px var(--gap-half) 5px var(--color-border);} In case you work with a Chromium based browser (Version 97.x as of today), you will be confronted with a bug. The “break” from one column to the next doesn’t respect the full 10px high shadow of the next item. It breaks too late. You will get something like this: I added a red shadow on hover, to make the bug more obvious. See following pen to inspect the CSS. In case you use Firefox (Version 96.x as of today), you won’t see the bug, because Mozilla did it right. WorkaroundStep 1In order to achieve a proper result, we have to hack the HTML and the CSS a bit. First of all we have to wrap the content of an item with a new element called ìtem-inner: 123456<div class="item"> <div class="item-inner"> Box 1 <img style="height:53px" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /> </div></div> Step 2Next, we move the width, the border and the box-shadow from the item itself to the new inner element. Important here is, to set the background of the item element to transparent in order to prevent interfering with the shadow. 123456789101112131415161718192021:root { --gap: 20px; --gap-half: calc(var(--gap) / 2); --color-border: hsl(0 0% 90%);}.item { /* BASIC STYLES */ background-color: transparent;}.item-inner { /* BASIC STYLES */ background-color: #fffaf5; border: 1px var(--color-border) solid; box-shadow: 1px 2px var(--gap-half) 5px var(--color-border); transition: all ease-out 0.3s; /* LAYOUT STYLES */ width: 200px; padding: 15px; overflow: hidden;} Step 3Now we have to set the wrapper‘s gap to 0, because we will implement the spacing of the items by applying different paddings to the item element: half of the gap to be reached, to the left, the right and the bottom padding and the full gap to the top. The latter, to achieve pushing the previous element far enough away so as not to see anything of the shadow. But because this would mean that the horizontal gap would be too large by half, we have to use a negative margin to pull the element up by that amount. This in turn would mean, that the first item element in the list would be too high by the now defined negative top margin, which we correct again by using item:first-child. 1234567891011121314151617181920212223:root { --gap: 20px; --gap-half: calc(var(--gap) / 2); /* ... more variables */}.wrapper { /* ...more styles */ column-gap: 0;}.item { /* ... BASIC STYLES */ /* LAYOUT STYLES */ width: auto; margin: var(--gap-half-negative) 0 0; padding: var(--gap) var(--gap-half) var(--gap-half); /* COLUMN STYLES */ break-inside: avoid-column;}.item:first-child { margin-top: 0;} Here is the complete solution as a pen: ConclusionMaybe the Chromium team will fix the bug as soon as possible, to no longer have to rely on this hack, but the really best solution would be, to finish the work already started on the true grid masonry, which currently only Firefox offers behind a flag. More Info caniuse.com: Can I Use: CSS property: grid-template-rowsRyan (dev.to): Creating A Responsive Masonry Layout Using The CSS column-count PropertyMDN WebDocs: Masonry LayoutRachel Andrew (Smashing Magazine): Native CSS Masonry Layout In CSS GridSyed Umar Anis: Create Masonry Layout with CSSChris Coyier (CSS-Tricks): Approaches for a CSS Masonry LayoutJonas (Kulturbanause): Responsive Masonry Layouts mit CSS erstellen (German)","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"}]},{"title":"GitHub Tag Plugins for Hexo","subtitle":null,"date":"2021-12-29","updated":"2021-12-29","path":"post/GitHub-Tag-Plugins-for-Hexo/","permalink":"https://kiko.io/post/GitHub-Tag-Plugins-for-Hexo/","excerpt":"Currently I’m working on improving my projects section by linking to some of my projects hosted on Github. One idea is to display the Github README there. Playing around with the GitHub API is fun and so I created two new Hexo Tag Plugins that I don’t want to deprive you of and that extend the Hexo Tag Plugin Collection. GitHub READMEGitHub User & Repo Card","keywords":"im working improving projects section linking hosted github idea display readme playing api fun created hexo tag plugins dont deprive extend plugin collection readmegithub user repo card","text":"Currently I’m working on improving my projects section by linking to some of my projects hosted on Github. One idea is to display the Github README there. Playing around with the GitHub API is fun and so I created two new Hexo Tag Plugins that I don’t want to deprive you of and that extend the Hexo Tag Plugin Collection. GitHub READMEGitHub User & Repo Card GitHub READMEGets the README.md file from a repo and shows it in an expandable detail tag. Usage Example: 1{% github_readme "kristofzerbe" "hexo-tag-plugins" "README for 'hexo-tag-plugins' on GitHub" %} Live Output: README for 'hexo-tag-plugins' on GitHub Hexo Tag PluginsHexo Tag Plugin Collection from kiko.io IntroductionHexo is a Markdown based SSG (Static Site Generator). Because the Mardown syntax is limited for good reason, Hexo has tag plugins you can use in your content to simplify and centralize complex structures, instead of writing pure HTML. For more information, see hexo.io/docs/tag-plugins. This project is a growing collection of tag plugins that I have developed and use for my blog kiko.io. Some of them are quite simple, others are more complex, but overall maybe helpful for you. Installation / UsingThere is no automatic installation of all tag plugins via NPM or other package managers, because every tag plugin stands for itself and you can pick the one you need simply by copying the appropriate JS file into your Hexo’s script folder: THEMES / <YOUR-THEME> / SCRIPTS. For every tag plugin in the list below I provide a Visual Studio Code Snippet to quickly insert a plugin into the content via the hotkey Ctrl+Space. To use the snippest create a new .code-snippets file in your projects .vscode folder and insert the snippets of all tag plugins you have downloaded into your project. Parameter Description SyntaxEvery tag plugin includes a description in the header how to use it. The syntax is as follows: Syntax Description param1 mandatory parameter [param2] optional parameter [param2=default] optional parameter with default value param:(option1,option2) parameter option list to choose one option ..."value1|value2" pipe delimitered array of values Plugins Anchor Anchorlist Alertbox Alternative Blockquote Blockquote Details Codepen CodeSandbox Download Link Github Readme GitHub User & Repo Card Image Compare Image Link Image Slide Indiepen More Info Image Masonry AnchorA simple anchor element as A- or HR-Tag as jump target for example from a Anchorlist. Files: tag-anchor.js Syntax: {% anchor "anchorId" elementType:(A,HR) %}`` Parameters: No Parameter optional/default Description 1 anchorId - String to define the anchor id 2 elementType - Type of tag to render; select out of A or HR Usage Example: {% anchor "my-anchor" HR %} Output: <hr id="my-anchor"> VS Code Snippet: "hexo.kiko-io.anchor": { "scope": "markdown", "prefix": "hexo.kiko-io.anchor", "body": [ "{% anchor \\"${1:anchorId}\\" ${2|A,HR|} %}" ], "description": "Insert kiko.io's anchor" } AnchorlistCreates an overview of all anchors in the content with jump links. Files: tag-anchorlist.js Syntax: {% anchorlist ..."title|anchorId" %} Parameters: No Parameter optional/default Description 1 ..."title|anchorId" - List of pipe separated items with referencing title and anchor id Usage Example: {% anchorlist "My First Anchor|a1" "My Second Anchor|a2" %} Output: <ul class="anchorlist"> <li data-anchor="#a1"> <a href="#a1">My First Anchor</a> </li> <li data-anchor="#a2"> <a href="#a2">My Second Anchor</a> </li> </ul> VS Code Snippet: "hexo.kiko-io.anchorlist": { "scope": "markdown", "prefix": "hexo.kiko-io.anchorlist", "body": [ "{% anchorlist ${1:...\\"title|anchorId\\"} %}" ], "description": "Insert kiko.io's anchorlist" } AlertboxRenders a iconized colored box with text for warnings or with some special information. 6 styles are provided: Exclamation, Question, Warning, Info, Success and Note. Files: tag-alertbox.js tag-alertbox.css Prequisites: The icons are from the font FontAwesome Free Solid, you need to reference in your CSS either from your project or from a CDN. You will find such references in the file tag-alertbox.css, together with all other necessary styles. Syntax: {% alertbox alertType:(exclamation,question,warning,info,success,note) %} content {% endalertbox %}`` Parameters: No Parameter optional/default Description 1 alertType - Type of the alert (icon and color); select out of exclamation, question, warning, info, success or note content is not a parameter, but Markdown to render. Usage Example: {% alertbox warning %} Something has failed! {% endalertbox %} Output: <div class="alertbox alertbox-warning"> <p>Something has failed!</p> </div> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#alertbox VS Code Snippet: "hexo.kiko-io.alertbox": { "scope": "markdown", "prefix": "hexo.kiko-io.alertbox", "body": [ "{% alertbox ${1|exclamation,question,warning,info,success,note|} %}", "${2:content}", "{% endalertbox %}" ], "description": "Insert kiko.io's alertbox" } Alternative BlockquoteAn alternative blockquote tag plugin for quotes with citator and reference url. Files: tag-blockquote_alt.js Syntax: {% blockquote_alt "cite" ["citeUrl"] %} quote {% endblockquote_alt %} Parameters: No Parameter optional/default Description 1 cite Author of the quote 2 citeUrl yes Url to the quote Usage Example: {% blockquote_alt "Anonymous" "https://en.wikipedia.org/wiki/Lorem_ipsum" %} Lorem ipsum dolor sit amet... {% endblockquote_alt %} Output: <div> <blockquote> <p>Lorem ipsum dolor sit amet…</p> </blockquote> <cite> <a href="https://en.wikipedia.org/wiki/Lorem_ipsum">--- Anonymous</a> </cite> </div> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#blockquote_alt VS Code Snippet: "hexo.kiko-io.blockquote": { "scope": "markdown", "prefix": "hexo.kiko-io.blockquote", "body": [ "{% blockquote_alt \\"${1:cite}\\" [\\"${2:citeUrl}\\"] %}", "${3:content}", "{% endblockquote_alt %}" ], "description": "Insert kiko.io's blockquote" } Blockquote DetailsBlockquote including summary, citator and reference url, wrapped in a details tag. Files: tag-blockquote_details.js Syntax: {% blockquote_details "summary" "cite" ["citeUrl"] %} quote {% endblockquote_details %} Parameters: No Parameter optional/default Description 1 summary - Summary of the quote 2 cite - Author of the quote 3 citeUrl yes Url to the quote quote is not a parameter, but Markdown to render. Usage Example: {% blockquote_details "Lorem ipsum" "Anonymous" "https://en.wikipedia.org/wiki/Lorem_ipsum" %} Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. {% endblockquote_details %} Output: <details> <summary>Lorem ipsum</summary> <blockquote> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. </p> </blockquote> <cite> <a href="https://en.wikipedia.org/wiki/Lorem_ipsum">--- Anonymous</a> </cite> </details> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#blockquote_details VS Code Snippet: "hexo.kiko-io.blockquote_details": { "scope": "markdown", "prefix": "hexo.kiko-io.blockquote_details", "body": [ "{% blockquote_details \\"${1:summary}\\" \\"${2:cite}\\" [\\"${3:citeUrl}\\"] %}", "${4:quote}", "{% endblockquote_details %}" ], "description": "Insert kiko.io's blockquote_details" } CodepenEmbedding a pen from codepen.io. Files: tag-codepen.js Prequisites: You need following configuration section in your _config.yml: # Codepen Defaults codepen: user_id: "your-name" default_tab: "js" height: 400 width: "100%" Syntax: {% codepen "slugHash" "title" [defaultTab:(html,js,css)] [height] ["width"] %} Parameters: No Parameter optional/default Description 1 slugHash - Codepens SlugHash 2 title - Title 3 defaultTab js Default tab to show ; select out of html, js or css 4 height 300 Height as number 5 width “100%” Width as CSS value Usage Example: {% codepen "abjJNYE" "Lorem Ipsum" html %} Output: <iframe height="400" id="codepen-abjJNYE" class="codepen" src="https://codepen.io/kristofzerbe/embed/abjJNYE?height=400&default-tab=html,result&theme-id=light" style="width: 100%;" scrolling="no" title="Codepen: Lorem Ipsum" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true"> </iframe> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#codepen VS Code Snippet: "hexo.kiko-io.codepen": { "scope": "markdown", "prefix": "hexo.kiko-io.codepen", "body": [ "{% codepen \\"${1:slugHash}\\" \\"${2:title}\\" [${3|html,js,css|}] [${4:height}] [\\"${5:width}\\"] %}" ], "description": "Insert kiko.io's codepen" } CodeSandboxEmbedding a sandbox from CodeSandbox. Files: tag-codesandbox.js Syntax: {% codesandbox "slugHash" "title" [height] ["width"] %} Parameters: No Parameter optional/default Description 1 slugHash - Sandbox’ SlugHash 2 title - Title 3 height 500 Height as number 4 width “100%” Width as CSS value Usage Example: {% codesandbox "cool-shamir-de613" "Lorem Ipsum" 300 %} Output: <iframe src="https://codesandbox.io/embed/cool-shamir-de613?fontsize=14&theme=light" style="width:100%; height:300px; border:0; overflow:hidden;" title="Lorem Ipsum" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"> </iframe> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#codesandbox VS Code Snippet: "hexo.kiko-io.codesandbox": { "scope": "markdown", "prefix": "hexo.kiko-io.codesandbox", "body": [ "{% codesandbox ${1:\\"slugHash}\\" \\"${2:title}\\" [${3:height}] [\\"${4:width}\\"] %}" ], "description": "Insert kiko.io's codesandbox" } Download LinkButton link for downloading an asset file, with additional caption (“Download <additionalCaption> <assetFile>”). Files: tag-download-link.js Syntax: {% download_link "assetFile" ["additionalCaption"] %} Parameters: No Parameter optional/default Description 1 assetFile Asset file name to download 2 additionalCaption yes Additional caption between “Download “ and file name Usage Example: {% download_link "example-image_ORIGINAL.jpg" "Photo" %} Output: <p class="download-link"> <a class="button" href="example-image_ORIGINAL.jpg" download=""> Download Photo <strong>example-image_ORIGINAL.jpg</strong> </a> </p> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#download-link VS Code Snippet: "hexo.kiko-io.download_link": { "scope": "markdown", "prefix": "hexo.kiko-io.download_link", "body": [ "{% download_link \\"${1:assetFile}\\" \\"${2:additionalCaption}\\" %}" ], "description": "Insert kiko.io's download_link" } Github ReadmeGets the README file of a Github repo and renders its Markdown as HTML in a detail tag. Files: tag-github-readme.js Prequisites: You need to have installed the Axios package in your project. The HTML output has no styles, therefore you need some in your CSS for .github-readme. Syntax: {% github_readme "user" "repo" ["summary"] %} Parameters: No Parameter optional/default Description 1 user Name of the GitHub user 2 repo Name of the GitHub repo 3 summary “Project README on Github” Caption of the DETAILS element Usage Example: {% github_readme "kristofzerbe" "hexo-tag-plugins" %} Output: <details class="github-readme"> <summary>Project README on Github</summary> <div> <!-- Content of the README file converted to HTML --> </div> </details> VS Code Snippet: "hexo.kiko-io.github_readme": { "scope": "markdown", "prefix": "hexo.kiko-io.github_readme", "body": [ "{% github_readme \\"${1:user}\\" \\"${2:repo}\\" \\"${3:[summary]}\\" %}" ], "description": "Insert kiko.io's github_readme" } See a live example at https://kiko.io/post/GitHub-Tag-Plugins-for-Hexo/#readme GitHub User & Repo CardRenders a card-like info panel, with full information about a GitHub repo and its creator, the GitHub user. Files: tag-github-user-and-repo-card.js tag-github-user-and-repo-card.css Prequisites: You need to have installed the Axios package in your project. Syntax: {% github_user_and_repo_card "user" "repo" ["cardWidth"] ["userheight"] ["avatarSize"] %} Parameters: No Parameter optional/default Description 1 user Name of the GitHub user 2 repo Name of the GitHub repo 3 cardWidth “400px” Maximum width of the card; Minimum is 300px 4 userheight “120px” Height of the upper user panel 5 avatarSize “90px” Size of the avatar image as CSS value Usage Example: {% github_user_and_repo_card "kristofzerbe" "hexo-tag-plugins" "500px" %} Output: <!-- see tag-github-user-and-repo-card.html --> VS Code Snippet: "hexo.kiko-io.github_user_and_repo_card": { "scope": "markdown", "prefix": "hexo.kiko-io.github_user_and_repo_card", "body": [ "{% github_user_and_repo_card \\"${1:user}\\" \\"${2:repo}\\" \\"${3:[cardWidth]}\\" \\"${4:[userhight]}\\" \\"${5:[avatarSize]}\\" %}" ], "description": "Insert kiko.io's github_user_and_repo_card" } See a live example at https://kiko.io/post/GitHub-Tag-Plugins-for-Hexo/#user-and-repo-card Image CompareComparing two asset images side-by-side with the aid of the JS library Image Compare Viewer. Files: tag-image-compare.js Prequisites: As this tag plugin relies on an external JS library, the files image-compare-viewer.js and image-compare-viewer.css (or its minified versions) must be loaded in the header of the web page. Syntax: {% image_compare "imgFileOriginal" "imgFileModified" "descriptionModified" [orientation=vertical] %} Parameters: No Parameter optional/default Description 1 imgFileOriginal - Original asset image file name 2 imgFileModified - Modified asset image file name 3 descriptionModified - Description 4 orientation null Vertical orientation Mode; set vertical to select Usage Example: {% image_compare "example-image_ORIGINAL.jpg" "example-image_PRESET.jpg" "Lightroom Preset" %} Output: <div id="image-compare-1yrasq"> <img class="image-compare image-original" src="/post/my-post/example-image_ORIGINAL.jpg" alt="" /> <img class="image-compare image-modified" src="/post/my-post/example-image_PRESET.jpg" alt="" /> </div> <script> var themeColor = "#ffffff"; if (localStorage.getItem("theme") === 'dark') { themeColor = "#222222" } new ImageCompare(document.getElementById("image-compare-1yrasq"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Lightroom Preset', onHover: true, } }).mount(); </script> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#image-compare VS Code Snippet: "hexo.kiko-io.image_compare": { "scope": "markdown", "prefix": "hexo.kiko-io.image_compare", "body": [ "{% image_compare \\"${1:imgFileOriginal}\\" \\"${2:imgFileModified}\\" \\"${3:modDesc}\\" [\\"${4|vertical|}\\"] %}" ], "description": "Insert kiko.io's image_compare" } Image LinkRenders an image including ALT attribute within a link. Files: tag-image-link.js Syntax: {% image_link "assetImg" "url" "alt" %} Parameters: No Parameter optional/default Description 1 assetImg - Asset image file name 2 url - Url to link to 3 alt - Alternate text Usage Example: {% image_link "kiko-io-screenshot.png" "http://kiko.io" "Blog kiko.io" %} Output: <a href="http://kiko.io"> <img src="kiko-io-screenshot.png" alt="Blog kiko.io"> </a> VS Code Snippet: "hexo.kiko-io.image_link": { "scope": "markdown", "prefix": "hexo.kiko-io.image_link", "body": [ "{% image_link \\"${1:assetImg}\\" \\"${2:url}\\" \\"${3:alt}\\" %}" ], "description": "Insert kiko.io's image_link" } Image SlideShows multiple images within a slider with the aid of the JS library Tiny Slider. Files: tag-image-slide.js Prequisites: As this tag plugin relies on an external JS library, the files tiny-slider.js and tiny-slider.css (or its minified versions) must be loaded in the header of the web page. The CSS file doesn’t include styles for the .tns-nav and its controls, but you can use the following to extend the original CSS: .tns-nav { text-align: center; margin: 10px 0; } .tns-nav > [aria-controls] { width: 12px; height: 12px; padding: 0; margin: 0 5px; border-radius: 50%; background: #ddd; border: 0; } .tns-nav > .tns-nav-active { background: #999; } Syntax: {% image_slide ..."assetImg|title" %} Parameters: No Parameter optional/default Description 1 ..."assetImg|title" List of pipe separated items with asset image file and title Usage Example: {% image_slide "example-image_ORIGINAL.jpg|Original" "example-image_PRESET.jpg|Lightroom Preset" %} Output: <div class="image-slider" id="image-slide-w7jgxk"> <div> <img src="/post/my-post/example-image_ORIGINAL.jpg" alt="Original" /> </div> <div> <img src="/post/my-post/example-image_PRESET.jpg" alt="Lightroom Preset" /> </div> </div> <script> tns({ container: "#image-slide-w7jgxk", items: 1, slideBy: "page", controls: false, nav: true }); </script> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#image-slide VS Code Snippet: "hexo.kiko-io.image_slide": { "scope": "markdown", "prefix": "hexo.kiko-io.image_slide", "body": [ "{% image_slide ${1:...\\"assetImg|title\\"} %}" ], "description": "Insert kiko.io's image_slide" } IndiepenEmbedding a “local” pen (index.html, main.js and styles.css stored in an asset subfolder) via Indiepen. Files: tag-indiepen.js Prequisites: You need following configuration section in your _config.yml: # Indiepen Defaults indiepen: default_tab: "result" height: 450 Syntax: {% indiepen "subfolder" [height] [defaultTab:(result,html,css,js)] %} Parameters: No Parameter optional/default Description 1 subfolder Asset subfolder with indiepen files 2 defaultTab result Default tab to show ; select out of result, html, js or css 3 height 450 Height as number Usage Example: {% indiepen "my-pen" 300 html %} Output: <iframe class="indiepen" src="https://indiepen.tech/embed/?url=https%3A%2F%2Fmy-blog.com%2Fpost%2Fmy-post%2Fmy-pen&tab=html" style="width: 100%; overflow: hidden; display: block; border: 0;" title="Indiepen Embed" loading="lazy" width="100%" height="300"> </iframe> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#indiepen VS Code Snippet: "hexo.kiko-io.indiepen": { "scope": "markdown", "prefix": "hexo.kiko-io.indiepen", "body": [ "{% indiepen \\"${1:subfolder}\\" ${2:height} ${3|result,html,css,js|} %}" ], "description": "Insert kiko.io's indiepen" } More InfoRenders a list of related, informative links regarding a post. Files: tag-moreinfo.js Syntax: {% moreinfo '{ "list": [ [ "publisher", "title", "url" ] ]}' %} Usage Example: {% moreinfo '{ "list": [ [ "Wikipedia", "Markdown", "https://en.wikipedia.org/wiki/Markdown" ], [ "Markdown Guide", "Basic Syntax", "https://www.markdownguide.org/basic-syntax/" ], [ "Daring Fireball", "Markdown: Syntax", "https://daringfireball.net/projects/markdown/syntax" ] ]}' %} Output: <ul class="moreinfo-list"> <li>Wikipedia: <a href="https://en.wikipedia.org/wiki/Markdown">Markdown</a></li> <li>Markdown Guide: <a href="https://www.markdownguide.org/basic-syntax/">Basic Syntax</a></li> <li>Daring Fireball: <a href="https://daringfireball.net/projects/markdown/syntax">Markdown: Syntax</a></li> </ul> See a live example at https://kiko.io/post/Hexo-Tag-Plugin-Collection/#more-info VS Code Snippet: "hexo.kiko-io.moreinfo": { "scope": "markdown", "prefix": "hexo.kiko-io.moreinfo", "body": [ "{% moreinfo '{ \\"list\\": [", " [ ${1:\\"publisher\\"}, ${2:\\"title\\"},", " ${3:\\"url\\"} ]$0", "]}' %}" ], "description": "Insert kiko.io's moreinfo" } To insert one more item to the list, use: "hexo.kiko-io.moreinfo.item": { "scope": "markdown", "prefix": "hexo.kiko-io.moreinfo.item", "body": [ "[ ${1:\\"publisher\\"}, ${2:\\"title\\"},", "${3:\\"url\\"} ]$0" ], "description": "Insert kiko.io's moreinfo item" } Image MasonryShows multiple images in a masonry grid with the aid of the JS library Macy.js. Files: tag-image-masonry.js Prequisites: As this tag plugin relies on an external JS library, the library file macy.js must be loaded in the header of the web page. Syntax: {% image_masonry ..."assetImg|title" %} Parameters: No Parameter optional/default Description 1 ..."assetImg|title" List of pipe separated items with asset image file and title Usage Example: {% image_masonry "example-image-1.jpg|First Image" "example-image-2.jpg|Second Image" "example-image-3.jpg|Third Image" "example-image-4.jpg|Fourth Image" "example-image-5.jpg|Fifth Image" "example-image-6.jpg|Sixth Image" "example-image-7.jpg|Seventh Image" "example-image-8.jpg|Eighth Image" %} Output: <div id="#image-masonry-z8katm"> <div><img src="example-image-1.jpg" alt="First Image"></div> <div><img src="example-image-2.jpg" alt="Second Image"></div> <div><img src="example-image-3.jpg" alt="Third Image"></div> <div><img src="example-image-4.jpg" alt="Fourth Image"></div> <div><img src="example-image-5.jpg" alt="Fifth Image"></div> <div><img src="example-image-6.jpg" alt="Sixth Image"></div> <div><img src="example-image-7.jpg" alt="Seventh Image"></div> <div><img src="example-image-8.jpg" alt="Eighth Image"></div> </div> <script> let macy = new Macy({ container: "#image-masonry-z8katm", trueOrder: false, waitForImages: false, useOwnImageLoader: false, debug: true, mobileFirst: true, columns: 2, margin: { y: 6, x: 6 }, breakAt: { 1024: { margin: { x: 8, y: 8 }, columns: 4 }, 768: 3 } }); </script> See a live example at https://kiko.io/post/Image-Masonry-Tag-Plugin-for-Hexo/ VS Code Snippet: "hexo.kiko-io.image_masonry": { "scope": "markdown", "prefix": "hexo.kiko-io.image_masonry", "body": [ "{% image_masonry ${1:...\\"assetImg|title\\"} %}" ], "description": "Insert kiko.io's image_masonry" } History2023-09-01 Image Masonry added 2021-12-29 Description of parameters added Github Readme added GitHub User & Repo Card added 2021-12-12 Initial Commit LicenseMIT : http://opensource.org/licenses/MIT See https://github.com/kristofzerbe/hexo-tag-plugins#github-readme for more details. GitHub User & Repo CardRenders a card-like info panel, with full information about a GitHub repo and its creator, the GitHub user. Usage Example: 1{% github_user_and_repo_card "kristofzerbe" "hexo-tag-plugins" "500px" %} Live Output: #gh-card-hexo-tag-plugins { --gh-card-width: 500px; --gh-user-height: 120px; --gh-avatar-size: 90px; } Kristof Zerbe kristofzerbe 33 Repos 9 Followers 3 2 1 0 hexo-tag-plugins Hexo Tag Plugin Collection from kiko.io JavaScript65.1% HTML18.2% CSS16.7% MIT License See https://github.com/kristofzerbe/hexo-tag-plugins#github-user--repo-card for more details.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"}]},{"title":"Discoveries #15 - Self Hosted","subtitle":null,"series":"Discoveries","date":"2021-12-25","updated":"2021-12-25","path":"post/Discoveries-15-Self-Hosted/","permalink":"https://kiko.io/post/Discoveries-15-Self-Hosted/","excerpt":"Especially on Github you can find amazing open source solutions for self hosting, that makes it unnecessary to rely on web services from companies you did not know. In this issue of Discoveries I would like to introduce you to a few of them … Happy Holidays AppFlowyBangle.ioCal.com (formerly Calendso)RSS-proxyFreshRSSStatsig's Status Pagechangedetection.ioHomerFiddlyFileDrop","keywords":"github find amazing open source solutions hosting makes unnecessary rely web services companies issue discoveries introduce … happy holidays appflowybangleiocalcom calendsorss-proxyfreshrssstatsig's status pagechangedetectioniohomerfiddlyfiledrop","text":"Especially on Github you can find amazing open source solutions for self hosting, that makes it unnecessary to rely on web services from companies you did not know. In this issue of Discoveries I would like to introduce you to a few of them … Happy Holidays AppFlowyBangle.ioCal.com (formerly Calendso)RSS-proxyFreshRSSStatsig's Status Pagechangedetection.ioHomerFiddlyFileDrop AppFlowy by -unknown- https://github.com/AppFlowy-IO/appflowy AppFlowy.IO is a Notion clone, written in Flutter and Rust und runs on macOS (with installer), Windows and Linux. Bangle.io by -unknown- https://github.com/bangle-io/bangle-io Bangle.io is a web based note taking platform, like Notion, but local only. It is written in TypeScript and relies on Markdown and works also offline. Cal.com (formerly Calendso) by Cal.com Team https://github.com/calendso/calendso Cal.com is an alternative scheduling service to Calendsy, driven by a company as a service, but also available as Open Source for selfhosting. It is written in JavaScript (Next.js). RSS-proxy by Github User 'damoeb' https://github.com/damoeb/rss-proxy RSS-proxy (demo), written in TypeScript, is a web service to create ATOM, RSS or JSON feeds by analyzing a websites static HTML structure. Helpful for websites, that doesn’t provide a feed. FreshRSS by Alexandre Alapetite & Others https://github.com/FreshRSS/FreshRSS FreshRSS (demo) is an alternative to feed.ly and other online RSS readers and aggregators, written in PHP. It supports custom tags, push notifications and extensions and has a CLI. Statsig's Status Page by statsig.com Team https://github.com/statsig-io/statuspage/ This open source status page solution (demo) uses Github actions to run a sh script every hour against configurable URL’s to check their status and log it in a static index.html. changedetection.io by Github User 'dgtlmoon' https://github.com/dgtlmoon/changedetection.io Web solution for monitoring configurable websites or JSON API’s for changes, written in Python. It detects changes, notifies and shows the differences. Homer by Bastien Wirtz https://github.com/bastienwirtz/homer A simple, but nice dashboard for your servers and services, configurable with YAML and written in Vue. Fiddly by Sara Vieira https://github.com/SaraVieira/fiddly Fiddly creates customizable HTML pages out of your Github projects README files for hosting on Github Pages , Netlify or others under a dedicated domain. FileDrop by Khodadad (Adrian) Nouchin https://github.com/Xtrendence/FileDrop FileDrop is an application to share files in the same network through a browser. It is written in JavaScript and Electron (Server) and is using WebSocket for encrypted transport. Releases are available for Windows, macOS and Linux.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Hexo Tag Plugin Collection","subtitle":"All tag plugins used for kiko.io available on Github","date":"2021-12-12","updated":"2021-12-12","path":"post/Hexo-Tag-Plugin-Collection/","permalink":"https://kiko.io/post/Hexo-Tag-Plugin-Collection/","excerpt":"Since day one of this blog I use Tag Plugins, sometimes as NPM packages from other developers, sometimes developed by myself. The latter have grown significantly over time and I want to share them with you by publishing them in a Github project called hexo-tag-plugins, where you can download and use those you need on extending your own Hexo based blog. On the Github page you can find all the info on how to use the plugins. In this article I will only briefly introduce them: AnchorAnchorlistAlertboxAlternative BlockqouteBlockquote DetailsCodepenCodeSandboxDownload LinkImage CompareImage LinkImage SlideIndiepenMore Info","keywords":"day blog tag plugins npm packages developers developed grown significantly time share publishing github project called hexo-tag-plugins download extending hexo based page find info article briefly introduce anchoranchorlistalertboxalternative blockqouteblockquote detailscodepencodesandboxdownload linkimage compareimage slideindiepenmore","text":"Since day one of this blog I use Tag Plugins, sometimes as NPM packages from other developers, sometimes developed by myself. The latter have grown significantly over time and I want to share them with you by publishing them in a Github project called hexo-tag-plugins, where you can download and use those you need on extending your own Hexo based blog. On the Github page you can find all the info on how to use the plugins. In this article I will only briefly introduce them: AnchorAnchorlistAlertboxAlternative BlockqouteBlockquote DetailsCodepenCodeSandboxDownload LinkImage CompareImage LinkImage SlideIndiepenMore Info AnchorAnchor element as A- or HR-Tag as jump target for example from a Anchorlist. Usage Example: 1{% anchor "my-anchor" HR %} Live Output: 1<hr id="my-anchor"> See https://github.com/kristofzerbe/hexo-tag-plugins#anchor for more details. AnchorlistCreates an overview of all anchors in the content with jump links. Usage Example: 1234{% anchorlist "My First Anchor|a1" "My Second Anchor|a2"%} Live Output: My First AnchorMy Second Anchor See https://github.com/kristofzerbe/hexo-tag-plugins#anchorlist for more details. AlertboxRenders a iconized colored box with text for warnings or with some special information. 6 styles are provided: Exclamation, Question, Warning, Info, Success and Note. This plugin uses a FontAwesome font for the icons and some styles that also need to be included in your Hexo project. Usage Example: 123{% alertbox warning %}Something has failed!{% endalertbox %} Live Output: Something has failed! Uuh, keep attention! Everything’s fine What’s up? Some important information Just as note… See https://github.com/kristofzerbe/hexo-tag-plugins#alertbox for more details. Alternative BlockquoteAn alternative blockquote tag plugin for quotes with citator and reference url. Usage Example: 123{% blockquote_alt "Anonymous" "https://en.wikipedia.org/wiki/Lorem_ipsum" %}Lorem ipsum dolor sit amet...{% endblockquote_alt %} Live Output: Lorem ipsum dolor sit amet… --- Anonymous See https://github.com/kristofzerbe/hexo-tag-plugins#alternative-blockquote for more details. Blockquote DetailsBlockquote including summary, citator and reference url, wrapped in a details tag. Usage Example: 123{% blockquote_details "Lorem ipsum" "Anonymous" "https://en.wikipedia.org/wiki/Lorem_ipsum" %}Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.{% endblockquote_details %} Live Output: Lorem ipsum Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. --- Anonymous See https://github.com/kristofzerbe/hexo-tag-plugins#blockquote-details for more details. CodepenEmbedding a pen from Codepen. Usage Example: 1{% codepen "abjJNYE" "Lorem Ipsum" html 250 %} Live Output: See https://github.com/kristofzerbe/hexo-tag-plugins#codepen for more details. CodeSandboxTag Plugin for embedding a sandbox from CodeSandbox. Usage Example: 1{% codesandbox "cool-shamir-de613" "Lorem Ipsum" 300 %} Live Output: See https://github.com/kristofzerbe/hexo-tag-plugins#codesandbox for more details. Download LinkButton link for downloading an asset file, with additional caption (“Download <additionalCaption> <assetFile>”). Usage Example: 1{% download_link "example-image_ORIGINAL.jpg" "Photo" %} Live Output: Download Photo example-image_ORIGINAL.jpg See https://github.com/kristofzerbe/hexo-tag-plugins#download-link for more details. Image CompareComparing two images side-by-side with the aid of the JS library Image Compare Viewer. Usage Example: 12345{% image_compare "example-image_ORIGINAL.jpg" "example-image_PRESET.jpg" "Lightroom Preset" %} Live Output: var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-c9iymj\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Lightroom Preset', onHover: true, } }).mount(); See https://github.com/kristofzerbe/hexo-tag-plugins#image-compare for more details. Image LinkRenders an image including ALT attribute within a link. Usage Example: 1{% image_link "kiko-io-screenshot.png" "http://kiko.io" "Blog kiko.io" %} Live Output: See https://github.com/kristofzerbe/hexo-tag-plugins#image-link for more details. Image SlideShows multiple images within a slider with the aid of the JS library Tiny Slider. Usage Example: 1234{% image_slide "example-image_ORIGINAL.jpg|Original" "example-image_PRESET.jpg|Lightroom Preset"%} Live Output: tns({ container: \"#image-slide-mgqq7g\", items: 1, slideBy: \"page\", controls: false, nav: true }); See https://github.com/kristofzerbe/hexo-tag-plugins#image-slide for more details. IndiepenEmbedding a “local” pen (index.html, main.js and styles.css stored in an asset subfolder) via Indiepen. Usage Example: 1{% indiepen "indiepen-example" 300 html %} Live Output: See https://github.com/kristofzerbe/hexo-tag-plugins#indiepen for more details. More InfoRenders a list of related, informative links regarding a post. Usage Example: 12345678{% moreinfo '{ "list": [ [ "Wikipedia", "Markdown", "https://en.wikipedia.org/wiki/Markdown" ], [ "Markdown Guide", "Basic Syntax", "https://www.markdownguide.org/basic-syntax/" ], [ "Daring Fireball", "Markdown: Syntax", "https://daringfireball.net/projects/markdown/syntax" ]]}' %} Live Output: Wikipedia: MarkdownMarkdown Guide: Basic SyntaxDaring Fireball: Markdown: Syntax See https://github.com/kristofzerbe/hexo-tag-plugins#more-info for more details.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"}]},{"title":"Tringula And The Beauty Of Mathematics","subtitle":"Triangula is a great tool for artfully breaking up images into polygons. Another tool also makes it possible to use them as website placeholders.","series":"Golem","date":"2021-12-07","updated":"2021-12-07","path":"post/Tringula-And-The-Beauty-Of-Mathematics/","permalink":"https://kiko.io/post/Tringula-And-The-Beauty-Of-Mathematics/","excerpt":"This post is a new version of Triangulate your images with Triangula and the first in a series of articles published on the German news site golem.de. When talking about triangulation, non-mathematicians generally understand it as a geometric method for measuring distances. Roughly speaking, two known points in space can be used to calculate a third via the angles to it. In one or the other Hollywood flick of the genres war or spy movie you have surely come across this. However, triangulation also refers to the division of a surface into triangles or, more generally, the description of an object by means of polygons. It is used in topology and land surveying, but also in imaging methods of modeling. How wonderfully this field of mathematics can be applied to photos is shown by the GitHub user RyanH with his program Triangula written in Go, which first roughly splits a given JPG or PNG image into triangles and then refines it further and further via mutations. Among other things, you can specify how many points you want to start with and how many mutations the program should perform. It is also possible to calculate the new image using hexagons instead of classic triangles.","keywords":"post version triangulate images triangula series articles published german news site golemde talking triangulation non-mathematicians generally understand geometric method measuring distances roughly speaking points space calculate angles hollywood flick genres war spy movie surely refers division surface triangles description object means polygons topology land surveying imaging methods modeling wonderfully field mathematics applied photos shown github user ryanh program written splits jpg png image refines mutations things start perform hexagons classic","text":"This post is a new version of Triangulate your images with Triangula and the first in a series of articles published on the German news site golem.de. When talking about triangulation, non-mathematicians generally understand it as a geometric method for measuring distances. Roughly speaking, two known points in space can be used to calculate a third via the angles to it. In one or the other Hollywood flick of the genres war or spy movie you have surely come across this. However, triangulation also refers to the division of a surface into triangles or, more generally, the description of an object by means of polygons. It is used in topology and land surveying, but also in imaging methods of modeling. How wonderfully this field of mathematics can be applied to photos is shown by the GitHub user RyanH with his program Triangula written in Go, which first roughly splits a given JPG or PNG image into triangles and then refines it further and further via mutations. Among other things, you can specify how many points you want to start with and how many mutations the program should perform. It is also possible to calculate the new image using hexagons instead of classic triangles. tns({ container: \"#image-slide-gyuepd\", items: 1, slideBy: \"page\", controls: false, nav: true }); The result is stylized images of the original, which can be used as a chic desktop background, for example. Due to the abstraction of the actual motif, such images are also very suitable as header images on websites, such as blogs, if the image should not distract from the actual content. Such an image has a great closeness to the original, but looks more like art. Besides PNG, SVG (Scalable Vector Graphics) is also available as output format, which makes sense because SVG is an XML format and the polygons calculated by the program can also be written away directly as corresponding polygon entries as text. The generated SVG files are thereby smaller by a factor of 30, depending on the original, and thus fit in many places much better with tight bandwidths or used-up consumption limits on the Internet. Using Triangula on the command lineRyan offers his program Triangula also as CLI version, which is called via the command line. The process of triangulating is separated into two parts: First you create the desired abstraction of an original image via the parameter run, which is stored in a JSON file on the hard disk and in a second step you create either a PNG or an SVG as output file via render and the specification of the JSON file. Since the command line tool has the same options as the UI version, it is great for automating the processing of the images used when building a website, for example. It also makes it much easier to turn an entire folder of images into such artistic abstractions. Using triangulated images as placeholders on websitesDepending on how good the Internet connection is and how well the developers brain of a website has worked, it can take a while until the browser of the smartphone has loaded the x-megabyte header or illustration image to finally be able to display it. It gets really annoying when no space has been reserved on the website for the image beforehand and the text that you have started to read suddenly jumps away. In any case, the solution is to always use images that are as small as possible and adapted to the device, but they still have a certain file size if they are to look good. To prevent text jumping, nowadays one usually uses gray placeholders with or without loading bars to signal to the user that something will be displayed at this point shortly. Ryan has found an amazingly effective solution for this as well: tip - Triagulated Placeholders - creating the smallest possible triangulated images and blending them using JavaScript. The basis is a frontend, also written in Go, which uses the same algorithms as Triangula and with which the user can process several original images or an entire folder of photos at once via the interface. The output format from tip is not PNG or SVG, but a binary file, which consumes the least amount of memory of all technical possibilities. For comparison the generated file sizes of the example image with resolution 1024 x 660: Image Size Original 386 KB Triangula PNG 223 KB Triangula SVG 40,3 KB tip TRI 3,03 KB These files, with the extension TRI, are referenced by the web developer in the IMG tag of an image on a page in the data-src attribute, and a JavaScript, only 200 lines long, which is also included and delivered with the web page, takes care of immediately displaying this TRI file when the page is loaded and smoothly fading to the original image once it has been loaded. You can hardly make it more beautiful.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"SVG","slug":"SVG","permalink":"https://kiko.io/tags/SVG/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"Discoveries #14","subtitle":null,"series":"Discoveries","date":"2021-11-20","updated":"2021-11-20","path":"post/Discoveries-14/","permalink":"https://kiko.io/post/Discoveries-14/","excerpt":"In this month discoveries you will find some posts and pens regarding CSS and its possibilities. It’s always amazing what can be done with it and what cool solutions can be found on the web. Sticky Definition ListsHow to Detect When a Sticky Element Gets PinnedDark mode in 5 minutes, with inverted lightness variablesFloat an Element to the Bottom CornerNeat Parallax Hero EffectUnderline animationBuilding split text animationsIntrinsic Typography is the Future of Styling Text on the WebCSS morphingCSS Tips","keywords":"month discoveries find posts pens css possibilities amazing cool solutions found web sticky definition listshow detect element pinneddark mode minutes inverted lightness variablesfloat bottom cornerneat parallax hero effectunderline animationbuilding split text animationsintrinsic typography future styling webcss morphingcss tips","text":"In this month discoveries you will find some posts and pens regarding CSS and its possibilities. It’s always amazing what can be done with it and what cool solutions can be found on the web. Sticky Definition ListsHow to Detect When a Sticky Element Gets PinnedDark mode in 5 minutes, with inverted lightness variablesFloat an Element to the Bottom CornerNeat Parallax Hero EffectUnderline animationBuilding split text animationsIntrinsic Typography is the Future of Styling Text on the WebCSS morphingCSS Tips Sticky Definition Lists by Chris Coyier https://css-tricks.com/sticky-definition-lists If you want to have an alphabetical definition list on your website, defined by dl, dt and dd tags, you can give your users a better usability on scrolling through this list by using Chris’ sample. How to Detect When a Sticky Element Gets Pinned by David Walsh https://davidwalsh.name/detect-sticky A very short but useful JS snippet from David Walsh on how to detect if an element is being pinned by CSS’ position:sticky. Dark mode in 5 minutes, with inverted lightness variables by Lea Verou https://lea.verou.me/2021/03/inverted-lightness-variables/ Providing a light and dark mode on websites is almost common today. Lea show us how we can save time on using HSL hue and lightness in CSS. Float an Element to the Bottom Corner by Temani Afif https://css-tricks.com/float-an-element-to-the-bottom-corner/ Placing illustration images in the text is a common way to lighten up a web page. Temani shows us how to place such an image at the bottom of a content element. Neat Parallax Hero Effect by Dominic Magnifico https://codepen.io/magnificode/pen/GpqGOm This Codepen from Dom shows us, how to shrink an hero image on scrolling a page down with little CSS and JavaScript. Underline animation by Aaron Iker https://codepen.io/aaroniker/pen/pojaBvb The default underlined links on web pages are really boring. Aaron gives us on his pen an animated alternative, which uses SVG and CSS. Building split text animations by Adam Argyle https://web.dev/building-split-text-animations/ Animating the title of a web site can be a nice way to add some movement to rigid text. Adam shows us how his approach to animating each character works. Intrinsic Typography is the Future of Styling Text on the Web by Scott Kellum https://css-tricks.com/intrinsic-typography-is-the-future-of-styling-text-on-the-web/ Flexible layouts have their pitfalls, especially in terms of adjusting text sizes. In this post, Adam talks about what he calls intrinsic typography, where text is scaled by using a Bézier curve to improve readability. CSS morphing by Amit Sheen https://codepen.io/amit_sheen/pen/xxqYzvm This pen from Amit is about a pure CSS technique on blending a word into another. Neat effect for countdowns for example. CSS Tips by Marko Denic https://markodenic.com/css-tips/ In this post Marko lists some useful tips on using pure CSS, including Typing Effect, Smooth Scrolling, Truncate text, CSS only modals, custom scrollbars, background-clip text or rounded gradient borders.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"VS Code on the Web","subtitle":"Multiple ways to work with Visual Studio Code online","date":"2021-10-22","updated":"2021-10-22","path":"post/VS-Code-on-the-Web/","permalink":"https://kiko.io/post/VS-Code-on-the-Web/","excerpt":"For most of the years I have been in the IT industry, I have worked with the “fat” Visual Studio from Microsoft. Fat in terms of features, for sure, but also in size and load times. It made no sense to use an other IDE, while developing software with VB.NET/C#. But with the advent of Node.JS JavaScript, so far only known as a scripting language for web pages, outgrew itself and became a serious competitor to established languages. In 2012 Adobe came out with Brackets, a lightweight IDE for developing web applications, written with the very same tech stack: HTML, CSS and JavaScript! Based on the Chromium Embedded Framework, it felt like a normal application! Mind blowing… In 2015 there was a new kid in town: Visual Studio Code (VS Code), of all things from … Microsoft. During this time, the Redmond-based company had finally jumped on the open source bandwagon and perhaps they saw that Adobe was doing some things right on the IDE market with Brackets (but also some things wrong) and you didn’t want to miss the chance to engage the open source community. The speed with which VS Code passed other IDE’s in the developer favor was quite amazing, due to the fact that the source code was openly available on GitHub and the developers in Switzerland released a new version every damn month. What was exciting for me was the question of how long it would take for someone to make this IDE based on web technology available online, i.e. in a browser. It took until 2021…","keywords":"years industry worked fat visual studio microsoft terms features size load times made sense ide developing software vbnet˼# advent nodejs javascript scripting language web pages outgrew competitor established languages adobe brackets lightweight applications written tech stack html css based chromium embedded framework felt normal application mind blowing… kid town code things … time redmond-based company finally jumped open source bandwagon market wrong didnt miss chance engage community speed passed ides developer favor amazing due fact openly github developers switzerland released version damn month exciting question long make technology online browser 2021…","text":"For most of the years I have been in the IT industry, I have worked with the “fat” Visual Studio from Microsoft. Fat in terms of features, for sure, but also in size and load times. It made no sense to use an other IDE, while developing software with VB.NET/C#. But with the advent of Node.JS JavaScript, so far only known as a scripting language for web pages, outgrew itself and became a serious competitor to established languages. In 2012 Adobe came out with Brackets, a lightweight IDE for developing web applications, written with the very same tech stack: HTML, CSS and JavaScript! Based on the Chromium Embedded Framework, it felt like a normal application! Mind blowing… In 2015 there was a new kid in town: Visual Studio Code (VS Code), of all things from … Microsoft. During this time, the Redmond-based company had finally jumped on the open source bandwagon and perhaps they saw that Adobe was doing some things right on the IDE market with Brackets (but also some things wrong) and you didn’t want to miss the chance to engage the open source community. The speed with which VS Code passed other IDE’s in the developer favor was quite amazing, due to the fact that the source code was openly available on GitHub and the developers in Switzerland released a new version every damn month. What was exciting for me was the question of how long it would take for someone to make this IDE based on web technology available online, i.e. in a browser. It took until 2021… github.dev and Github CodespacesIn Juli 2021 GitHub announced the availability of github.dev and GitHub Codespaces. The main difference between these two solutions is that Codespaces runs the IDE within a container (VM) in the background, which enables you to run your project and … it is only available for paid plans. The main purpose of github.dev is to serve as a call target of the so-called Magic Dot, an easy way to open any repository in an editor. I blogged about this capabilty a while ago, see GitHubs Magic Dot. Really amazing! Just press the dot key on every repository and you can browse the code files. vscode.devRecently, in October 2021, Microsoft (who owns GitHub) announced another online VS Code called vscode.dev. It is practically the same IDE as github.dev, with one main difference: It is not bound to a GitHub repository, but is able to open any local project or even remote repositories from GitHub. However, it also has the same limitation that you cannot run a project, because there is no VM running in the background. But it is a really neat online editor, which runs on mobile devices too and feels absolutely like a local installed VS Code. GitpodBack in 2017 some developers from Kiel, Germany started a web-based platform called Gitpod for providing fully functional orchestrated developer environments in the web. Since at that time VS Code was not yet running in the browser, they started the project Eclipse Theia, which powers several online IDE’s until today, but switched to VS Code as the team around Erich Gamma announced remote development capabilities in late 2020. Whats special about Gitpod is, that users are able to start a browser-based instance of the IDE just by adding the address of an GitHub repository as a parameter to the URL, like https://gitpod.io#https://github.com/kristofzerbe/kiko.io. Gitpot then starts a container with the source code and shows up the IDE at a random URL like https://coffee-squirrel-htamfigy.ws-eu18.gitpod.io. This so called Workspaces can be stopped, resumed, shared and downloaded, because it is a container with everything in it you need to run. Really amazing! Other Monaco-driven IDE’sVS Code actually consists of two parts: the platform itself, called Code-OSS, and the code editor Monaco included in it, which is also available as a separate project and used by other web-based IDE’s, like the following… StackblitzThe IDE on stackblitz.com is mainly useful for web frontend developers. You can easily create Angular, React, Vue, Next.js, Nuxt or even plain JavaScript or static HTML projects and connect them to an new repository on GitHub. But … you can’t load existing projects from GitHub into Stackblitz on the fly, you have to import them. What makes Stackblitz very comfortable is that it runs your frontend directly on their servers and gives it to you in a browser-like preview window via a random Url like https://web-platform-ywqj4s.stackblitz.io, you can open up in a separate browser also. CodeSandboxCodeSandbox works similar to Stackblitz, but offers some more features, like deployment to Vercel, Netlify or GitHub Pages and a test runner. Also, you have full control over the sandbox that runs the preview of your project and the ability to invite other developers or visitors to the project, which makes it perfect for online coding seminars e.g. classrooms.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"}]},{"title":"Croatian Presets for Lightroom","subtitle":null,"series":"Lightroom Presets","date":"2021-10-16","updated":"2021-10-16","path":"post/Croatian-Presets-for-Lightroom/","permalink":"https://kiko.io/post/Croatian-Presets-for-Lightroom/","excerpt":"Staying in Croatia is always a joy, in summer but also in winter. I’m on this side of the Adriatic sea almost every year. In summer you have a pleasant heat, inviting you to swim, and sometimes too many tourists. In winter you have the magic light and space to enjoy the country. Over the past years a have created some presets to bring my images, shot in Croatia, to a next level of beauty and I want to share them with you in this post.","keywords":"staying croatia joy summer winter im side adriatic sea year pleasant heat inviting swim tourists magic light space enjoy country past years created presets bring images shot level beauty share post","text":"Staying in Croatia is always a joy, in summer but also in winter. I’m on this side of the Adriatic sea almost every year. In summer you have a pleasant heat, inviting you to swim, and sometimes too many tourists. In winter you have the magic light and space to enjoy the country. Over the past years a have created some presets to bring my images, shot in Croatia, to a next level of beauty and I want to share them with you in this post. Croatian Warm SeaThe sea in Croatia is amazing clear because almost the entire coast is made of stone instead of sand. There is hardly any algae or other things that cloud the wonderful green-blue water. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-iugagu\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Croatian Warm Sea.xmp Croatian BrightnessYes, I’m guilty: I love colors and contrast! If you do also, his preset is for you. It brings out any image that is too flat or dull by helping to control the brightness and contrast. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-m0u4x3\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Croatian Brightness.xmp Croatian Winter DramaA drive along the coast in the morning or evening hours makes you want to stop all the time because one panorama is more beautiful than the next. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-alptw1\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Croatian Winter Drama.xmp Croatian Winter CityThe walls of Split have this wonderful warm tone of the winter sun and this preset brings this out particularly well. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-jxdy8c\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Croatian Winter City.xmp Croatian Winter SunsetThe sunset in Croatia is special for me, because I feel these wonderful colors and my camera isn’t able to reproduce this feeling. This preset is an approximation of it. The walls of Split have this wonderful warm tone of the winter sun and this preset brings this out particularly well. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-xwqe82\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Croatian Winter Sunset.xmp","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Presets","slug":"Presets","permalink":"https://kiko.io/tags/Presets/"}]},{"title":"The Last Image Gallery...","subtitle":"... you have to deal with: Spotlight","series":"Great Finds","date":"2021-10-10","updated":"2021-10-10","path":"post/The-Last-Image-Gallery/","permalink":"https://kiko.io/post/The-Last-Image-Gallery/","excerpt":"In the last decade(s) I have seen and tried many image galleries and lightboxes for showing images or groups of images. Depending on your needs, you can choose out of trillions of solutions, for every JS framework or vanilla JS, in every flavour, size and color. With many of them, however, you reach the limits quite quickly. Be it in terms of visual adaptability, extensibility or implementation. Customization cost time and nerves, especially if the respective library has structural weaknesses. However, from today on, I don’t need to look for a suitable solution for my next project, because I found one that leaves absolutely none of my wishes unfulfillede: Spotlight by Nextapps from Berlin, Germany. To make it clear: this is not a paid advertising text or something like that. That wouldn’t make sense either, because Spotlight is Open Source (Apache 2.0 License) and its code is availabel at GitHub. I’m just thrilled with the work of the developers.","keywords":"decades image galleries lightboxes showing images groups depending choose trillions solutions js framework vanilla flavour size color reach limits quickly terms visual adaptability extensibility implementation customization cost time nerves respective library structural weaknesses today dont suitable solution project found leaves absolutely wishes unfulfillede spotlight nextapps berlin germany make clear paid advertising text wouldnt sense open source apache license code availabel github im thrilled work developers","text":"In the last decade(s) I have seen and tried many image galleries and lightboxes for showing images or groups of images. Depending on your needs, you can choose out of trillions of solutions, for every JS framework or vanilla JS, in every flavour, size and color. With many of them, however, you reach the limits quite quickly. Be it in terms of visual adaptability, extensibility or implementation. Customization cost time and nerves, especially if the respective library has structural weaknesses. However, from today on, I don’t need to look for a suitable solution for my next project, because I found one that leaves absolutely none of my wishes unfulfillede: Spotlight by Nextapps from Berlin, Germany. To make it clear: this is not a paid advertising text or something like that. That wouldn’t make sense either, because Spotlight is Open Source (Apache 2.0 License) and its code is availabel at GitHub. I’m just thrilled with the work of the developers. The first step on implementing every image gallery solution is the installation. Spotlight has different options, but the easiest one is to download the bundled version, which includes both the JS files and the image (icon) and CSS files. For the non-bundles files, you can choose the minified versions or even the original ES6 and LESS files. There is a NPM package also. For the implementation you can either choose a declarative way using the spotlight class, where the shown thumb images to click on needs to have a wrapper… 123456<a class="spotlight" href="img1.jpg"> <img src="thumb1.jpg"></a><a class="spotlight" href="img2.jpg"> <img src="thumb2.jpg"></a> … or programmatically via JavaScript: 12345var gallery = [ { src: "img1.jpg" }, { src: "img2.jpg" }];Spotlight.show(gallery /*, options */); Next to the image, at the bottom left corner, a title, a description and/or a button can be displayed on a gallery slide, which is completely compatible with all types of devices. It is possible to define image groups to show separately in the declaration mode by having an extra wrapper around a bunch of image wrappers. All options how the gallery has to be shown are also declarative: 123456789101112<div class="spotlight-group" data-title="Group title"> <a class="spotlight" href="img1.jpg" data-title="This is a title"> <img src="thumb1.jpg"> </a> <a class="spotlight" href="img2.jpg" data-title="This is another title" data-description="This is a description"> <img src="thumb2.jpg"> </a></div> Spotlight has 9 built-in controls to show in the bar at the top left: Fullscreen Zoom in Zoom out Autofit Close Theme Play (Slideshow) Download … and there is a possibility to insert custom controls during initialization. If you provide several image sizes, Spotlight picks the optimal version regarding the current devices resolution, pixel ratio and bandwidth. But Spotlight is not limited to images. It can also display videos and even custom HTML fragments (DOM nodes) as slides. Through its JavaScript API you can fully remote control the gallery and it has several options to customize the appearance of the slides to every need you can imagine. The implementation is a breeze. It took me less than 10 minutes to get Spotlight working here on this blog at the photo page! I can’t think of any use case right now, that you couldn’t implement easily and quickly with this amazing library. Congrats folks … extremely well done!","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"Discoveries #13","subtitle":null,"series":"Discoveries","date":"2021-09-29","updated":"2021-09-29","path":"post/Discoveries-13/","permalink":"https://kiko.io/post/Discoveries-13/","excerpt":"This month, Discoveries is all about JavaScript-driven “components” that you can quickly and easily add to your own website to enhance it. Be it with a simple code viewer or an ingenious print function or simply to display or filter photos. Go on a journey of discovery… indiepenPanzoomguggenheim.jsLazy Loading Mosaic Tiling PluginScrollTriggerWinBox.jsPrint.jsSimple Text AnnotationsClicky Menus!Responsive Dropdown Menu (Vanilla Navbar Menu)Smooth-side-barPodtablejs","keywords":"month discoveries javascript-driven components quickly easily add website enhance simple code viewer ingenious print function simply display filter photos journey discovery… indiepenpanzoomguggenheimjslazy loading mosaic tiling pluginscrolltriggerwinboxjsprintjssimple text annotationsclicky menusresponsive dropdown menu vanilla navbar menusmooth-side-barpodtablejs","text":"This month, Discoveries is all about JavaScript-driven “components” that you can quickly and easily add to your own website to enhance it. Be it with a simple code viewer or an ingenious print function or simply to display or filter photos. Go on a journey of discovery… indiepenPanzoomguggenheim.jsLazy Loading Mosaic Tiling PluginScrollTriggerWinBox.jsPrint.jsSimple Text AnnotationsClicky Menus!Responsive Dropdown Menu (Vanilla Navbar Menu)Smooth-side-barPodtablejs indiepen by Hendrik and André from yetanother.blog https://indiepen.tech/ indiepen is a solution for showing code samples without the need of a code sharing platform, like codepen. Just reference a index.html, main.js and styles.css from wherever you want and indiepen is wrapping it with a neat viewer inside an IFrame. Panzoom by Cesar Morillas https://github.com/cmorillas/panzoom Implementing panning and zooming with JavaScript is not the easiest thing. Cesar has done all he work by creating this tiny ES6 module. It works both on a smartphone and in a desktop browser via mouse wheel. It is nearly perfect to zoom into an image to show its details. Lazy Loading Mosaic Tiling Plugin by Christopher Peloso https://github.com/cspeloso/Lazy-Loading-Mosaic-Tiling-Plugin Do you have a bunch of images you want to show in a masonry layout on your website? Whith Chris’ JS library it’s just a one-liner. It supports responsiveness and lazy-loading in tiny 132 lines of JS code and 4 CSS classes. Amazing! guggenheim.js by Will McKenzie http://oinutter.co.uk/guggenheim.js/ Another approach on showing images in a gallery is Guggenheim.js. It’s not responsive, but has sophisticated filtering options. Perfect for quickly finding a keyworded photo. ScrollTrigger by Greensock Inc. https://greensock.com/scrolltrigger/ Greensocks ScrollTrigger is unparalleled among scroll animation libraries. Based on its own GSAP library, ScrollTrigger can animate almost everthing, when the user is scolling through a website. Want to see it in action? Visit their demo page with dozens of examples… WinBox.js by Nextapps GmbH https://nextapps-de.github.io/winbox/ The guys from Nextapps have created a JavaScript library to show windows on a website. It’s window manager has features like minimize, maximize, move, resize, fullscreen and much more. Always wanted to recreate Windows 3.11? Let’s go … Print.js by Rodrigo Vieira https://printjs.crabbly.com/ Print.js is a library, that helps you printing anything, which can be shown in a browser. Primarily written to print PDF, it supports now also HTML (including forms), JSON and all sorts of images, even multiple. It formats the wanted content if needed and shows up the browsers print dialog. Pretty neat. Simple Text Annotations by Jacek Jarczok https://github.com/k-son/simple-text-annotations Some texts on websites require commenting. Jacek has developed a simple and elegant solution for such annotations, that works on any device without any dependencies. Clicky Menus! by Mark Root-Wiley https://github.com/mrwweb/clicky-menus Mark has created a one-level dropdown navigation menu, which is fully accessible, either by mouse click, touch or keyboard. It supports all Modern Browsers such as Firefox, Chrome, Edge, and even the “new IE” Safari. Responsive Dropdown Menu (Vanilla Navbar Menu) by Rizal https://github.com/therizaldev/vanilla-navbar-menu This classic responsive sidebar from Rival is beautifully solved with CSS and just a little JavaScript. See the demo … Smooth-side-bar by Pham Quang Huy https://github.com/dunbom6612/smooth-side-bar This sidebar solution from Pham Quang Huy recalls the sidebars Microsoft, Atlassian and others are using in their dashboards. You can fold/unfold them in order to show menu details or the icons only. See the demo … Podtablejs by Afuwape Sunday https://github.com/inlogicstudio/podtable Podtable is a library to make tables responsive to fit smaller devices. It shows as many columns as possible and hides all others behind a detail button to show them in a separate row if needed. It has no dependencies and varius options to customize the behaviour. See the demo …","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Uups ... empty posts","subtitle":"Problems with post asset files in Hexo and a solution approach","date":"2021-09-21","updated":"2021-09-21","path":"post/Uups-empty-posts/","permalink":"https://kiko.io/post/Uups-empty-posts/","excerpt":"A while ago I wrote about Automatic Duplicate Image Shadow and used indiepen for showing the result of my efforts. indiepen is a solution for showing code samples without the need of a code sharing platform, like codepen. Just reference a index.html, main.js and styles.css from wherever you want and indiepen is wrapping it with a neat viewer inside an IFrame. I did it quick and dirty first (sample files in a static folder) and now it was the time to do it right: place the sample files in a subfolder of the post in my Hexo-driven blog solution, in order to reference it from there AND have the possibility to call it directly via ./post/my-post/sample. The key to achive that in Hexo is the configuration option post_asset_folder: true, which generates a subfolder for all assets with the same name as the post. 12345|- _posts |- my-blog-post |- my-first-asset.jpg |- my-second-asset.jpg |- my-blog-post.md My idea regarding the indiepen files was having a subfolder for each indiepen in the post asset folder: 123456789|- _posts |- my-blog-post |- my-first-asset.jpg |- my-second-asset.jpgNEW |- sampleNEW |- index.htmlNEW |- main.jsNEW |- styles.css |- my-blog-post.md Run hexo generate, check that the indiepen was showing up properly and I thought I was done. Wrong … after commiting my changes to Github, where my blog is living, and checking my RSS feed a while after, I saw this: Three empty posts…!?","keywords":"ago wrote automatic duplicate image shadow indiepen showing result efforts solution code samples sharing platform codepen reference indexhtml mainjs stylescss wrapping neat viewer inside iframe quick dirty sample files static folder time place subfolder post hexo-driven blog order possibility call directly /post/my-post/sample key achive hexo configuration option post_asset_folder true generates assets 12345|- _posts |- my-blog-post my-first-assetjpg my-second-assetjpg my-blog-postmd idea asset 123456789|- my-second-assetjpgnew samplenew indexhtmlnew mainjsnew run generate check properly thought wrong … commiting github living checking rss feed empty posts…","text":"A while ago I wrote about Automatic Duplicate Image Shadow and used indiepen for showing the result of my efforts. indiepen is a solution for showing code samples without the need of a code sharing platform, like codepen. Just reference a index.html, main.js and styles.css from wherever you want and indiepen is wrapping it with a neat viewer inside an IFrame. I did it quick and dirty first (sample files in a static folder) and now it was the time to do it right: place the sample files in a subfolder of the post in my Hexo-driven blog solution, in order to reference it from there AND have the possibility to call it directly via ./post/my-post/sample. The key to achive that in Hexo is the configuration option post_asset_folder: true, which generates a subfolder for all assets with the same name as the post. 12345|- _posts |- my-blog-post |- my-first-asset.jpg |- my-second-asset.jpg |- my-blog-post.md My idea regarding the indiepen files was having a subfolder for each indiepen in the post asset folder: 123456789|- _posts |- my-blog-post |- my-first-asset.jpg |- my-second-asset.jpgNEW |- sampleNEW |- index.htmlNEW |- main.jsNEW |- styles.css |- my-blog-post.md Run hexo generate, check that the indiepen was showing up properly and I thought I was done. Wrong … after commiting my changes to Github, where my blog is living, and checking my RSS feed a while after, I saw this: Three empty posts…!? The indiepen files in the asset subfolder were treated like posts by Hexo by no obvious reason! A short research on the web lead me to a Github issue called asset files were rendered when post_asset_folder set to true from 2015, with several requests to fix the bug. Since I’m little familiar with Hexo’s processors, I saw two ways to fix the problem: Write a plugin like Hexo Hide Posts to filter out unwanted “posts” after processing Try to find the problem in Hexo’s source code, to avoid these files to be treated as posts a priori After a little searching around in Hexo’s source code, I was able to identify the location where the problem was. In the post processor: ./node_modules/hexo/lib/plugins/processor/post.js (shortened)1234567891011121314151617181920212223242526272829303132333435363738module.exports = ctx => { ... return { pattern: new Pattern(path => { if (isTmpFile(path)) return; let result; if (path.startsWith(postDir)) { result = { published: true, path: path.substring(postDir.length) }; } else if (path.startsWith(draftDir)) { result = { published: false, path: path.substring(draftDir.length) }; } if (!result || isHiddenFile(result.path)) return; result.renderable = ctx.render.isRenderable(path) && !isMatch(path, ctx.config.skip_render); return result; }), process: function postProcessor(file) { if (file.params.renderable) { return processPost(file); } else if (ctx.config.post_asset_folder) { return processAsset(file); } } };};... In the very beginning of processing posts, the code iterates over all files and folders in the _posts folder and creates a param object for each by using the pattern. This object has an attribute named renderable, which is used by the process method later on, to decide if it has to be rendered or not. But … the determination of renderable is based solely on wether a file has a renderer (isRenderable) or if it is listed in the skip_render configuration. See line 24 in the sample code. As HTML, JS and CSS files naturally have a renderer in Hexo, they will treated as posts! Alleviation Of The ProblemIn this early stage of code execution, only files and folders are listed. There is no reference to a post or something like that, in order to find out if a certain file belongs to a post as an asset. Examination of the path will fail, because it is possible in Hexo to have a folder hierarchy under _posts. The only way to approach the problem solution is by making assumptions: If the user has activated the configuration post_asset_folder, he wants to have asset files in a folder named after the post If the user has configured a certain file name for new posts in new_post_name (for example :title.md), it is not to be assumed, that he will create asset files with the same file extension. Important here is, that it is not an option to hide the files in some way, because then they won’t appear in the website. They have to be marked as process it, but don’t render it, by bending the renderable attribute. These considerations led to a small extension of the above code below line 24: ./node_modules/hexo/lib/plugins/processor/post.js (shortened)12345678910...result.renderable = ctx.render.isRenderable(path) && !isMatch(path, ctx.config.skip_render);//if post_asset_folder is set, restrict renderable files to default file extension if (result.renderable && ctx.config.post_asset_folder) { result.renderable = (extname(ctx.config.new_post_name) === extname(path));}... This is what I pushed as a Pull Request to the Hexo team today. I know, it not ideal, but maybe they accept my approach and ship the fix with the next version.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"Photo Workflow Re-Thought","subtitle":null,"series":"Step By Step","date":"2021-09-12","updated":"2021-09-12","path":"post/Photo-Workflow-Re-Thought/","permalink":"https://kiko.io/post/Photo-Workflow-Re-Thought/","excerpt":"To be honest: many of my posts here on kiko.io are written just for me. To internalize things by writing them down and to give my future me the chance to look up something I did in the past. So is this post. Future me: Don’t forget the following! I while ago I was on a trip, shooting a lot of photographs and on my way back home I had three 32GB SD cards full with great photos. I was working with my old Nikon D7000, which has 2 card slots and I just took them out, when one of the cards was full. Worked fine for several years … but after this trip, one of the cards, full with RAW files and wonderful photos, w-a-s N-O-T r-e-a-d-a-b-l-e a-n-y-m-o-r-e … f***! I saved myself the backup and now had to suffer over my carelessness. I thought about some fancy and expensive backup solutions for professional photographers, but realized after a while, that I already had the equipment to achieve everything I needed and I could even use it to improve my general workflow. In this post, I want to show you, what my workflow looks like today and how yours might benefit from my mistake.","keywords":"honest posts kikoio written internalize things writing give future chance past post dont forget ago trip shooting lot photographs back home 32gb sd cards full great photos working nikon d7000 card slots worked fine years … raw files wonderful w-a-s   n-o-t   r-e-a-d-a-b-l-e   a-n-y-m-o-r-e f*** saved backup suffer carelessness thought fancy expensive solutions professional photographers realized equipment achieve needed improve general workflow show today benefit mistake","text":"To be honest: many of my posts here on kiko.io are written just for me. To internalize things by writing them down and to give my future me the chance to look up something I did in the past. So is this post. Future me: Don’t forget the following! I while ago I was on a trip, shooting a lot of photographs and on my way back home I had three 32GB SD cards full with great photos. I was working with my old Nikon D7000, which has 2 card slots and I just took them out, when one of the cards was full. Worked fine for several years … but after this trip, one of the cards, full with RAW files and wonderful photos, w-a-s N-O-T r-e-a-d-a-b-l-e a-n-y-m-o-r-e … f***! I saved myself the backup and now had to suffer over my carelessness. I thought about some fancy and expensive backup solutions for professional photographers, but realized after a while, that I already had the equipment to achieve everything I needed and I could even use it to improve my general workflow. In this post, I want to show you, what my workflow looks like today and how yours might benefit from my mistake. PrerequisitesHardware Nikon D500amazon.com Sony Professional XQD G Series 64GB Memory Cardamazon.com Prograde Digital SD UHS-II 64GB Cardamazon.com The Nikon D500 has 2 different card slots: XQD and SD, where the SD is configured in the camera as backup medium. It is important to use a SD card that is as big and as fast, in terms if writing speed, as the used XQD card! Lenovo Yoga Smart Tab (Android)amazon.com This tablet has a Micro SD card slot for extending the internal memory up to 128 GB (inofficially up to 2TB) and a USB type C socket. Rocketek Type C XQD/SD Card Readeramazon.com This card reader has two slots for XQD and SD and a USB Type C connector with a short cable. … and any Windows 10 machine Services Dropboxdropbox.com Apps for Android (Tablet) Solid Explorer File Managerplay.google.com Photo Mate R3play.google.com Autosync for Dropbox - Dropsyncplay.google.com Apps for Windows 10 Adobe Lightroom Classic (CC)adobe.com The Workflow In GeneralThe aim of the workflow is to transfer the photos from the camera to other hardware on the one hand and to cloud-based storage on the other, from where they can then be easily moved to the actual storage medium at home. In the end, always 2 different backups will exist on different media. When traveling, it is important to always have the tablet and the card reader with you, in addition to the camera and some spare SD cards. Step 1Whenever it is necessary or advisable, take both cards out of the camera and put the XQD card into the card reader and connect the latter to the tablet. Just put the SD card away, as we will use a spare card afterwards. Open up Solid Explorer on the tablet… Copy the complete folder DCIM/101ND500 (where the photos are stored) to a separate folder called RAW on the disk (SD card extension) of the tablet. Step 2Rename the new folder on the disk to something appropriate:(Screenshot says Rename a file, but that’s a bug in the app) Step 3Insert the XQD card and a spare SD card back into the camera and wipe them both via the built-in menu. Your backup is now on the orginal SD cards and your tablet. Step 4 (optional)If you have time and leisure, you can use Photo Mate R3 to review and rate your images. Don’t delete any photos in this step!Just mark photos with RED, which can be removed afterwards, because the app is sometimes not fast enough to delete the right photo, when you has opened the next one! Step 5Open up Dropsync on the tablet and set up a new sync profile, if you haven’t done so already: The remote folder is a folder called RAW in your Dropbox and the local folder is the RAW folder on your disk. Sync method should be Upload then delete, because you don’t need the second backup on your tablet, after the photos are transfered to Dropbox. Run the synchronization: Your backup is now on the original SD cards and Dropbox. Step 6After you have enjoyed your trip and be back home, move the photo folder from your Dropbox to your local storage, which of course should also have a backup of some kind: Your backup is now on the original SD cards and your local storage. Step 7If you’ve already rated your images along the way, Photo Mate R3 has already stored that metadata in an XMP sidecar file, that is compatible with Lightroom: Open up Lightroom and import the folder from your local storage: Step 8In the Lightroom library, filter all photos with the RED flag and set them as REJECTED: Step 9Delete all photos marked as REJECTED: Step 10If you are sure that your local backup was done once, you can safely wipe the original SD cards, in order to use them for the next time.","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Workflow","slug":"Workflow","permalink":"https://kiko.io/tags/Workflow/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"Application-Specific Links on Windows 10","subtitle":"What works and what we still have to wait for","date":"2021-09-03","updated":"2021-09-03","path":"post/Application-Specific-Links-on-Windows-10/","permalink":"https://kiko.io/post/Application-Specific-Links-on-Windows-10/","excerpt":"While reading the Chris Coyier’s post Application-Specific Links the other day, I realized what has been bugging me for a long time now: a proper solution for openening a certain URL in a modern Web App. Since the beginning of the digital age (feels like that), we have files associated to a certain application, installed on our machine, regardless if its running Windows, iOS, OS/2 or whatever. We have learned that well and no one questions it … but … the IT world keeps on turning and today we are not only talking about files, but about links. Many modern applications are written with Web technologies, thanks to cross platform frameworks like Electron. Some of them are real apps for working on things, like the famous editor Visual Studio Code, and some are mirroring their online services in a desktop app only, like Slack or Notion. However, the latter have the problem how to deal with links to their online services. When a user is sent a link and has become accustomed to using the desktop app, it won’t open in the app as he clicks on it, but in his default browser. The question is, how to associate not only files, but links with certain desktop apps?","keywords":"reading chris coyiers post application-specific links day realized bugging long time proper solution openening url modern web app beginning digital age feels files application installed machine running windows ios os˲ learned questions … world turning today talking applications written technologies cross platform frameworks electron real apps working things famous editor visual studio code mirroring online services desktop slack notion problem deal user link accustomed wont open clicks default browser question associate","text":"While reading the Chris Coyier’s post Application-Specific Links the other day, I realized what has been bugging me for a long time now: a proper solution for openening a certain URL in a modern Web App. Since the beginning of the digital age (feels like that), we have files associated to a certain application, installed on our machine, regardless if its running Windows, iOS, OS/2 or whatever. We have learned that well and no one questions it … but … the IT world keeps on turning and today we are not only talking about files, but about links. Many modern applications are written with Web technologies, thanks to cross platform frameworks like Electron. Some of them are real apps for working on things, like the famous editor Visual Studio Code, and some are mirroring their online services in a desktop app only, like Slack or Notion. However, the latter have the problem how to deal with links to their online services. When a user is sent a link and has become accustomed to using the desktop app, it won’t open in the app as he clicks on it, but in his default browser. The question is, how to associate not only files, but links with certain desktop apps? Executables and URL SchemesTriggered by Notions announcement of using a dedicated link protocol notion:// to open up Notion links directly in the Notion App, Chris wrote about the possibilities an a Mac to associate certain URL schemes with a Web App in general. His choice fell on Choosy, an app which hooks into to the process on opening links by beeing the systems default browser and forwarding the URL to configured applications. Fair enough, but it’s a tool for Mac. However, it was not particularly difficult to find equivalent programs for Windows 10, my preferred OS: Browser Selector Browser Select Browser Picker Browser Choose 2 … name yours Interestingly, Microsoft already has something similar built into Windows, but prevents users from messing with it: The feature is targeted from UWP applications only, therefore the developer has to implement it by extending the app manifest: Web-to-App Linking with AppUriHandlers. What Chris’ approach and all similar Windows programs have in common is, that they are based on executables, EXE files and yes, the cross-platform apps mentioned above are executables, but for me this is too short. What about Chromium based Progressive Web Apps (PWA)? Since the Chromium team has introduced the possibility to add a website as desktop shortcut and start it in a new window, without any browser specific toolbars and other stuff, it became very popular, especially if this website was implemented by the developer as a PWA.It does not matter whether all PWA features, such as offline mode or similar, are available. The mere fact of being able to use an online service in its own window is a gain. But such an app has no specific executable! It will be executed by the browser with some parameters itself. The question is, how to associate a certain URL scheme or a custom protocol with these web apps? PWA’s on WindowsSome basics about how Chromium based browsers are dealing with this feature: As you click on Create Shortcut… in the More Tools menu in Chrome or Edge and confirm the following dialog (or install the PWA), a new folder in %LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Web Applications will be created. Its name starts with _crx_ and is followed by a random string (so called app-id), f.e. ffokdlainpppngbbhdcobaocmbobgdii. In this folder the Favicon of the website is stored. If it is a real PWA and has a Web App Manifest, you will find all resources defined there in another subfolder named by the app-id below %LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Web Applications\\Manifest Resources. The paths differ depending on which browser profile is active. In this case ‘default’. The desktop shortcut, which will be created, points to an executable named chrome_proxy.exe or msedge_proxy.exe. These files are scaled-down versions of the browser executable itself. Its main purpose is to bypass a Windows error as described in its source code, in order to create a correct shortcut. The important part of the shortcut are the calling parameters: --app-id - the random ID generated by the browser --profile-directory - the browser profile to use; mostly ‘default’ If you are interested, Chromium has dozens of parameters, some to change the behaviour and others only for debugging purposes. Peter Beverloo provides an automatic generated List of all Chromium Command Line Switches. … a bit deeper, just for funThe following has nothing to do with the actual problem and doesn’t help in any way, but it was interesting to dig a bit deeper into the operating principles of Chromium. As we saw that the browser creates a unique ID for the new shortcut, we find all needed data referenced in the file %LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\preferences. It has no extension, but it is a JSON file. Two sections are relevant in this file: browser.app_window_placement._crx_<app-id> Holds the information how the window is displayed on the desktop (metrics). preferences > browser.app_window_placement1234567891011"_crx_ffokdlainpppngbbhdcobaocmbobgdii": { "bottom": 889, "left": 161, "maximized": false, "right": 1610, "top": 56, "work_area_bottom": 920, "work_area_left": 0, "work_area_right": 1707, "work_area_top": 0} extensions.settings.<app-id> Holds every other information about the app, including the complete manifest und the URL to show. preferences > extensions.settings (shortened)12345678910111213141516171819202122232425262728293031323334353637383940414243444546"ffokdlainpppngbbhdcobaocmbobgdii": { "active_bit": false, "active_permissions": { "api": [], "manifest_permissions": [] }, "app_launcher_ordinal": "zzo", "commands": {}, "content_settings": [], "creation_flags": 17, "events": [], "from_bookmark": true, "from_webstore": false, "granted_permissions": { "api": [], "manifest_permissions": [] }, "incognito_content_settings": [], "incognito_preferences": {}, "install_time": "13266055455495996", "launchType": 3, "locallyInstalled": true, "location": 1, "manifest": { "app": { "display_mode": "browser", "icon_color": "#186AA5", "launch": { "web_url": "https://trello.com" }, "linked_icons": [ ... ] }, "description": "Organize anything, together. Trello is a collaboration tool that organizes your projects into boards. In one glance, know what's being worked on, who's working on what, and where something is in a process.", "icons": { ... }, "key": "", "name": "Trello", "version": "2021.5.21.20218" }, "page_ordinal": "y", "path": "ffokdlainpppngbbhdcobaocmbobgdii\\\\2021.5.21.20218_0", "preferences": {}, "regular_only_preferences": {}, "state": 1, "was_installed_by_default": false, "was_installed_by_oem": false}, It is possible to edit the preferences manually, but not advisable, because its the backbone of your browser profile and you have to restart all browser instances after changes. Registering Protocol Handlers via JavaScriptThere are native protocol handlers in every OS. The most famous is mailto:. A click on such a link opens the default mail editor. Since 2006 the W3C discusses an extension called registerProtocolHandler, every web app could implement to tell the OS, that it is responsible for a certain protocol. 123navigator.registerProtocolHandler("web+myfancyapp", "https://my-fancy-web-app.com/?url=%s", "My Fancy App Handler"); If this registrations as the user enters the site for the first time, he will be asked by the browser for allowance, he has to confirm to make it work. As the developer implements the links, he has to write it like that: 1<a href="web+myfancyapp:settings">Go To Settings</a> The registration and a click on such a link causes the browser to request https://my-fancy-web-app.com/?url=settings. For understandable reasons some protocols are blacklisted, like mailto, irc, tel and others. Custom protocol schemes has to have the prefix web+, in order to avoid interference with the standardized protocols. As of May 2021, it is part of the capabilities project and is in development currently. Nice … but doesn’t helps in solving our problem in the first place, because we can’t yet break out of the browser with it. We need something to tell the browser that an installed PWA is responsible for this protocol… Manifest Protocol HandlersThere is no official W3C proposal, but experiments around the Chromium team to extend the PWA’s manifest file like that: web.manifest123456"protocol_handlers": [ { "protocol": "web+myfancyapp", "url": "/?url=%s" }] This could be the bridge we need to bind a PWA to a certain protocol, in order to open up the registered browser app, instead of a new tab. But its is just an experiment. Manifest URL HandlersAnother approach on connecting certain links to a PWA is similar to the manifests protocol_handlers but for URL schemes. No need for registering a protocol … browser just parse the URL and show up my app: web.manifest12345678"url_handlers" : [ { "origin": "https://my-fancy-app.com" }, { "origin": "https://*.my-fancy-app.com" }] Simple and straightforward. There is a Chrome Platform Page for this feature and the current status is Origin Trial for version 93! To try it out, you can enable #enable-desktop-pwas-url-handling in Edge and Chrome: But … it is also not even on the way to an official proposal :| ConclusionAs of today there is no solution for Windows users to associate links with the bunch of different applicationson windows, as we are used to from the files. But there is a special technique regarding web apps at the horizon, zo ease this pain, even if it depends on the app developers. Maybe Microsoft should think about their approach of Apps for Websites, in order to make it possible for users to extend that. The cherry on top would be then, if they decide to integrate the upcoming Protocol and URL Handlers, defined by the developers, right into this tool. More Info whatwg.org: HTML - Living StandardMDB Web Docs: Web-based protocol handlersThomas Steiner (web.dev): URL protocol handler registration for PWAsMatt West: Registering Protocol Handlers to Intercept Special LinksGyuyoung: What is navigator.registerProtocolHandler?GitHub Issue on W3C/Manifest: Add manifest option for PWAs to be registered as scheme/protocol handlersMDN Web Docs: protocol_handlersMicrosoft Docs: Experimental features in Progressive Web Apps (PWAs)GitHub MicrosoftEdge/MSEdgeExplainers: URL Protocol Handler Registration for PWAs","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"},{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"PWA","slug":"PWA","permalink":"https://kiko.io/tags/PWA/"}]},{"title":"Pattern for dynamic Hexo pages","subtitle":"Set up pages with dynamic data easily","date":"2021-08-25","updated":"2021-08-25","path":"post/Pattern-for-dynamic-Hexo-pages/","permalink":"https://kiko.io/post/Pattern-for-dynamic-Hexo-pages/","excerpt":"Hexo is a great SSG platform for blogging. Just write your Markdown beneath some Frontmatter meta data, run hexo generate and publish the results to a web server. But at some point you may want to process different data from internal or external sources and integrate it into your blog. Hexo doesn’t support this out of the box, but has a powerful feature called Generator, which helps you to achieve your goal. The following is a sample and pattern of how to implement this. The starting point of my example is the requirement to display several elements of the same type on a dynamic page, but you can of course adapt the example according to your needs.","keywords":"hexo great ssg platform blogging write markdown beneath frontmatter meta data run generate publish results web server point process internal external sources integrate blog doesnt support box powerful feature called generator helps achieve goal sample pattern implement starting requirement display elements type dynamic page adapt","text":"Hexo is a great SSG platform for blogging. Just write your Markdown beneath some Frontmatter meta data, run hexo generate and publish the results to a web server. But at some point you may want to process different data from internal or external sources and integrate it into your blog. Hexo doesn’t support this out of the box, but has a powerful feature called Generator, which helps you to achieve your goal. The following is a sample and pattern of how to implement this. The starting point of my example is the requirement to display several elements of the same type on a dynamic page, but you can of course adapt the example according to your needs. The PageFirst of all we need a Hexo page for the meta data and for some text we want to show at the beginning of the page to describe what is shown below. In order not to interfer the classic post generation, we create a new folder in Hexo’s source folder called _dynamic, where we place the MD file for the new dynamic page. The underscore in the name of the folder _dynamic is important, because Hexo doesn’t touch subfolders in source with this starting character while generating the site. Without, it would be treated as a normal page. ./source/_dynamic/my-special-page.md123456789---title: My Titlesubtitle: My Subtitledate: 2021-08-24 19:24:00mySpecialProperty: "Lorem ipsum dolor sit amet"---Some text to show above the dynamic created content... The Frontmatter data has no limits. You can add as much properties as you like. The Layout TemplateHexo works with EJS layout files. Whenever a page should be created, the calling method has to define which layout file should be used. Therefor we create a special EJS file for our dynamic page and place it in the layout folder. ./themes/<your-theme>/layout/my-special-layout.ejs1234567891011121314151617<h1 class="page-title"><%= page.title %></h1><h2 class="page-subtitle"><%= page.subtitle %></h2><div class="page-content"> <%- page.content %> <div class="view grid"> <% for(var i=0; i < page.items.length; i++) { %> <%- partial('_partial/my-special-item', { item: page.items[i] }) %> <% } %> </div> </div><script> // Extend the page with JavaScript ...</script> On generating the page later on, an object called page will be used to process the EJS, with all information we want to show on the page, like content (text of the Markdown file) and all meta data from the Frontmatter (title, subtitle and so on). Special attention is paid to items, because this property holds the list of all items we want to show on the page. Each object in this list have multiple custom properties you define, when you are assemble the data to show in the generator, but more about that later. In the script tag you can write JavaScript to interact with your data on the page, like filtering stuff or other things the user should can do with your data. The Partial Template For ItemsIn order not to blow up the layout EJS, it is advisable to separate the template for the item itself to an extra file, referenced in the layout file (_partial/my-special-item) as a Partial. ./themes/<your-theme>/layout/_partial/my-special-item.ejs123<div class="my-item"> <!-- Definition of an item --></div> The layout EJS will iterate over the list of items in the FOR loop and handover one of these object to the Partial to render. The GeneratorAs we have made our preparations, we are now able to implement our special generator itself. Hexo uses so called Generators to “produce” the HTML for your site and you can add your own by using the register method. The result of a generator has to be an object with at least three properties: data - Data object with all necessary information to process the given layout EJS path - The path where the HTML file should be created layout - The layout EJS file, which should be processed ./themes/<your-theme>/scripts/my-special-generator.js123456789101112131415161718hexo.extend.generator.register("my-special-generator", async function(locals) { // INSTANTIATE A NEW DATA OBJECT let page = {}; page.name = "my-special-page"; // Do something to get content and data for the page // INSTANTIATE THE RESULT OBJECT let result = { data: page, path: path.join(page.name, "index.html"), layout: "my-special-layout" } // RETURN THE RESULT return result; }); In this basic structure, you will find again the layout file, we created earlier. The output will be rendered in an HTML file called index.html at the subfolder my-special-page, as we take the name of the page for it, as we did it for the name of the source MD file. The next thing we have to implement here, is the content of the source MD file. The parameter locals gives you access to the site variables, with all information about the site and its pages, posts, categories and tags! The first 4 lines in the following script are the references to some helpers we need. Please be sure, that you install them first via npm install. ./themes/<your-theme>/scripts/my-special-generator.js12345678910111213141516171819202122232425262728293031323334353637383940414243const log = require('hexo-log')({ debug: false, silent: false });const path = require('path');const fs = require('hexo-fs');const front = require('hexo-front-matter');hexo.extend.generator.register("my-special-generator", async function(locals) { // GET REFERENCE TO HEXO'S CONFIGURATION let config = this.config; // SHOW MESSAGE ON GENERATING log.info("Processing items for dynamic page"); let page = {}; page.name = "my-special-page"; // GET THE PATH TO THE SOURCE FILE const mdSource = path.join(config.source_dir, "_dynamic", page.name + ".md"); // GET THE CONTENT OF THE SOURCE FILE const md = fs.readFileSync(mdSource); // PARSE THE FRONTMATTER OF THE SOURCE FILE let fm = front.parse(md); // ADD THE FRONTMATTER TO THE DATA OBJECT page = {...page, ...fm}; // CONVERT MARKDOWN CONTENT OF THE SOURCE FILE INTO HTML page.content = hexo.render.renderSync({ text: page._content, engine: 'markdown' }); // Do something to get items data for the page //page.items = []; //... let result = { data: page, path: path.join(page.name, "index.html"), layout: "my-special-layout" } return result; }); The only thing missing now, is your implemention of filling page.items with a list of objects you want to show on the page. There are no limits to your imagination. Get data from external API’s or process JSON data, stored in the data folder … or whatever you prefer. A live example of this approach are the pages TINY TOOLS (processing data from the Trello API) and PHOTOS here on this blog.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"Visualize the codebase of your GitHub repo","subtitle":"Try GitHub Next's repo-vizualizer action for generating a diagram of your codebase","date":"2021-08-21","updated":"2021-08-21","path":"post/Visualize-the-codebase-of-your-GitHub-repo/","permalink":"https://kiko.io/post/Visualize-the-codebase-of-your-GitHub-repo/","excerpt":"Beginning of the month, Amelia Wattenberger of GitHub Next has published a project to create a SVG visualization of a GitHub repository’s codebase. On the project page Visualizing a codebase, she talks about the advantages of code vizualization in terms of a better overview and comparability of code … and I loved it at first sight, because I’m an absolute visual person. But her attempt was not only to show us what’s possible (static SVG files and even interactive apps for code browsing, filtering and comparing), but give us the possibility to create our own codebase diagrams as SVG automatically, whenever we commit our code, by running a GitHub Action, she and her team has developed … the Repo Vizualizer","keywords":"beginning month amelia wattenberger github published project create svg visualization repositorys codebase page visualizing talks advantages code vizualization terms overview comparability … loved sight im absolute visual person attempt show whats static files interactive apps browsing filtering comparing give possibility diagrams automatically commit running action team developed repo vizualizer","text":"Beginning of the month, Amelia Wattenberger of GitHub Next has published a project to create a SVG visualization of a GitHub repository’s codebase. On the project page Visualizing a codebase, she talks about the advantages of code vizualization in terms of a better overview and comparability of code … and I loved it at first sight, because I’m an absolute visual person. But her attempt was not only to show us what’s possible (static SVG files and even interactive apps for code browsing, filtering and comparing), but give us the possibility to create our own codebase diagrams as SVG automatically, whenever we commit our code, by running a GitHub Action, she and her team has developed … the Repo Vizualizer Actually, her instructions are quite simple to implement, but the devil is in the details and I would like to show you what you may need for this. The goal is to prepare every project hosted on GitHub with instructions to run the Repo Visualizer after every commit to create or update a SVG file in the project, we can use in the README or via hotlinking in every other web page. Let’s start with my setup: Windows 10 Visual Studio Code a bunch of tiny projects hosted on GitHub Project IntegrationGitHub actions are configured via YAML files in the folder .github\\workflows. Therefore, just take Amelias demo file diagram.yaml and copy it to this folder. .github\\workflows\\diagram.yml12345678910111213141516name: Create Vizualizing Codebase Diagramon: workflow_dispatch: {} push: branches: - mainjobs: get_data: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@master - name: Update diagram uses: githubocto/repo-visualizer@main with: excluded_paths: "ignore,.github" You don’t have to change anything in this file, except the name if you want to. It is clear to run correctly. Prepare WindowsIn case you get the following error message on pushing the new file in your project to GitHub … 1refusing to allow an OAuth App to create or update workflow .github/workflows/diagram.yml without workflow scope … you have a problem with your OAuth Token the Git Credential Manager created for you, while installing Git for Windows. This token doesn’t include the permission to update GitHub action workflows. A workaround is, to create a Personal Access Token on Github WITH this permission … … and replace the existing token in the Windows Credential Manager (in German the wonderful word “Anmeldeinformationsverwaltung”): ParametersThere are a few interesting parameters to place within the with section, that allow you to customize the output of the graph to your needs: output_file - The name and relative path to the SVG file to generate. Default is ./diagram.svg root_path - The root path of the code to be vizualized excluded_paths - Folders to exclude from visualizing (as in the demo YAML) excluded_globs - Files to exclude from visualizing in micromatch syntax commit_message - After the action has created the diagram, it will be commited with this custom message After pushing your changes, you can watch the action run in the ACTIONS tab in GitHub: The READMEAs the SVG is part of yor project, you can use it easily in your repos README: README.md123## Codebase VizualizationFor an inactive diagram, please visit [Repo Visualization App](https://octo-repo-visualization.vercel.app/?repo=kristofzerbe%2Fhexo-generator-anything)...![Visualization of the codebase](./DIAGRAM.svg) This SVG is not interactive as Amelias Repo Visualization React app is, but it is a good overview of a repos codebase und it looks really good.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"GitHubs Magic Dot","subtitle":"New experience in code editing","date":"2021-08-20","updated":"2021-08-20","path":"post/GitHubs-Magic-Dot/","permalink":"https://kiko.io/post/GitHubs-Magic-Dot/","excerpt":"GitHub has released the long awaited solution für code editing in the web today, or more precisely, the two new solutions: CodeSpaces and github.dev. As Brigit Murtaugh and Allison Weins pointed out in their presentation at the live stream on Youtube (VS Code anywhere: GitHub Codespaces and github.dev), both solutions are based on the codebase of Visual Studio Code, but have different approaches and target groups. Where Codespaces is an online editor with computing capabilities (realized by running a VM in the backend) and is only available für paid plans, github.dev is a free online editor to change files in your GitHub repo as you do it in your local VSCode, but without running and debugging capabilities.","keywords":"github released long awaited solution für code editing web today precisely solutions codespaces githubdev brigit murtaugh allison weins pointed presentation live stream youtube based codebase visual studio approaches target groups online editor computing capabilities realized running vm backend paid plans free change files repo local vscode debugging","text":"GitHub has released the long awaited solution für code editing in the web today, or more precisely, the two new solutions: CodeSpaces and github.dev. As Brigit Murtaugh and Allison Weins pointed out in their presentation at the live stream on Youtube (VS Code anywhere: GitHub Codespaces and github.dev), both solutions are based on the codebase of Visual Studio Code, but have different approaches and target groups. Where Codespaces is an online editor with computing capabilities (realized by running a VM in the backend) and is only available für paid plans, github.dev is a free online editor to change files in your GitHub repo as you do it in your local VSCode, but without running and debugging capabilities. Features like extensions, code completion, references, creating pull requests and other things we need all day in our VSCode, works also in these web editors, including settings sync! github.devThe non-computing web editor is fully integrated in GitHub itself. There is no URL github.dev, where you can load your repo. Instead, if the URL to your repo is f.e. https://github.com/myname/myrepo, just replace .com with .dev and your code is available in the editor! For people who like it even more comfortable: go to your repo and just press the DOT key. Really smart. This even works on mobile, which will definitely change my workflow…","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"}]},{"title":"Discoveries #12 - Tutorials","subtitle":"Some great articles from CSS masters","series":"Discoveries","date":"2021-07-30","updated":"2021-07-30","path":"post/Discoveries-12-Tutorials/","permalink":"https://kiko.io/post/Discoveries-12-Tutorials/","excerpt":"This months discoveries it’s all about CSS tutorials. You will find some of the best articles of profound CSS masters like Ahmad Shadeed or Josh Comeau. All tips are vital to bring your CSS to the next level. Happy learning… CSS Variables 101Full-Bleed Layout Using CSS GridA Deep Dive Into CSS Grid minmax()Create Diagonal Layouts Like It's 2020Using calc to figure out optimal line-heightDrop-Shadow: The Underrated CSS FilterA Guide to the Responsive Images Syntax in HTMLCentering in CSSHow to trigger a CSS animation on scrollCSS Scroll Snap","keywords":"months discoveries css tutorials find articles profound masters ahmad shadeed josh comeau tips vital bring level happy learning… variables 101full-bleed layout grida deep dive grid minmaxcreate diagonal layouts 2020using calc figure optimal line-heightdrop-shadow underrated filtera guide responsive images syntax htmlcentering csshow trigger animation scrollcss scroll snap","text":"This months discoveries it’s all about CSS tutorials. You will find some of the best articles of profound CSS masters like Ahmad Shadeed or Josh Comeau. All tips are vital to bring your CSS to the next level. Happy learning… CSS Variables 101Full-Bleed Layout Using CSS GridA Deep Dive Into CSS Grid minmax()Create Diagonal Layouts Like It's 2020Using calc to figure out optimal line-heightDrop-Shadow: The Underrated CSS FilterA Guide to the Responsive Images Syntax in HTMLCentering in CSSHow to trigger a CSS animation on scrollCSS Scroll Snap CSS Variables 101 by Ahmad Shadeed https://ishadeed.com/article/css-vars-101/ CSS variables (or ‘custom properties’) are a huge helper to structure your CSS code and make it more maintainable. Ahmad shows us how to use their complete power. Full-Bleed Layout Using CSS Grid by Josh Comeau https://www.joshwcomeau.com/css/full-bleed/ Today it can be tricky to layout websites for small devices like smartphones and very large desktop screens. Josh shows us how to utilize CSS Grid on a classic 3-column layout the smart way. A Deep Dive Into CSS Grid minmax() by Ahmad Shadeed https://ishadeed.com/article/css-grid-minmax/ Ahmad again. In this tutorial he brings us a deeper look into the MINMAX() function, which is not easy to understand and to use. Create Diagonal Layouts Like It's 2020 by Nils Binder https://9elements.com/blog/pure-css-diagonal-layouts/ Over 90% of all websites have an easy to understand rectangular layout, I guess. Nils shows us how to bring some disruptions into those layouts, by implementing diagonal sections, to make them less boring. Using calc to figure out optimal line-height by Jesús Ricarte https://kittygiraudel.com/2020/05/18/using-calc-to-figure-out-optimal-line-height/ Text on the web must be easy to read and one of the probably most underrated options on styling text is the line height. Jesús shows us, how to calculate the line height for an optimal reading experience. Drop-Shadow: The Underrated CSS Filter by Michelle Barker https://css-irl.info/drop-shadow-the-underrated-css-filter/ There are two shadow properties in CSS: box-shadow and drop-shadow. Michelle show us how to use the one or the other properly. A Guide to the Responsive Images Syntax in HTML by Chris Coyier https://css-tricks.com/a-guide-to-the-responsive-images-syntax-in-html/ Chris must not be missing in a list of CSS tutorials. As CSS has a bunch of tags and properties to show images in responsive layouts, he shows us how to use them effectively. Centering in CSS by Adam Argyle https://web.dev/centering-in-css/ Centering elements can be a mess, but not for Adam who shows all possibilities with PRO’s and CON’s. How to trigger a CSS animation on scroll by Nick Ciliak https://coolcssanimation.com/how-to-trigger-a-css-animation-on-scroll/ Animations, cleverly used, give a page the finishing touches. But it is important to control them. Nick shows us how to achieve this using CSS and a bit JavaScript. CSS Scroll Snap by Ahmad Shadeed https://ishadeed.com/article/css-scroll-snap/ And finally once again Ahmad. In this tutorial he deals comprehensively with CSS’s scroll-snap feature.","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"Tutorial","slug":"Tutorial","permalink":"https://kiko.io/tags/Tutorial/"}]},{"title":"Running Rollup with Gulp","subtitle":"Use Rollup.js as JavaScript bundler in your Gulp pipeline","date":"2021-07-29","updated":"2021-07-29","path":"post/Running-Rollup-with-Gulp/","permalink":"https://kiko.io/post/Running-Rollup-with-Gulp/","excerpt":"Writing an SPA (Single Page Application) in JavaScript/CSS always means to keep an eye on small files to deliver. Especially when utilizing a bunch of libraries and frameworks, bundling is some sort of a must. The offer on bundlers and task runners is large on the web: WebPack, Snowpack, Browserify, Parcel, Grunt, Gulp and “DingDong” (just replace with the hotest new shit available). But, it is not always necessary to replace your complete building pipeline, when the new “DingDong” is hyped in the media. Brave old tools like Gulp are doing their job pretty well … and you are able to integrate some more modern approaches on bundling JS, for example. I couple of months ago, while working on a private project, I became attentive to Rollup.js, a next-generation JavaScript module bundler from Rich Harris, the author of Svelte. Rollup uses the new standardized format for code modules included in the ES6 revision of JavaScript and supports Tree-Shaking, which means that it analyzes all your ES6 imports statements and bundles only the code which is used. Pretty cool … but … it is a JavaScript bundler only and there are no plugins for Gulp, my favourite task runner. In this article I will show you, how to integrate Rollup in your Gulp bundling pipeline.","keywords":"writing spa single page application javascript˼ss means eye small files deliver utilizing bunch libraries frameworks bundling sort offer bundlers task runners large web webpack snowpack browserify parcel grunt gulp dingdong replace hotest shit complete building pipeline hyped media brave tools job pretty … integrate modern approaches js couple months ago working private project attentive rollupjs next-generation javascript module bundler rich harris author svelte rollup standardized format code modules included es6 revision supports tree-shaking analyzes imports statements bundles cool plugins favourite runner article show","text":"Writing an SPA (Single Page Application) in JavaScript/CSS always means to keep an eye on small files to deliver. Especially when utilizing a bunch of libraries and frameworks, bundling is some sort of a must. The offer on bundlers and task runners is large on the web: WebPack, Snowpack, Browserify, Parcel, Grunt, Gulp and “DingDong” (just replace with the hotest new shit available). But, it is not always necessary to replace your complete building pipeline, when the new “DingDong” is hyped in the media. Brave old tools like Gulp are doing their job pretty well … and you are able to integrate some more modern approaches on bundling JS, for example. I couple of months ago, while working on a private project, I became attentive to Rollup.js, a next-generation JavaScript module bundler from Rich Harris, the author of Svelte. Rollup uses the new standardized format for code modules included in the ES6 revision of JavaScript and supports Tree-Shaking, which means that it analyzes all your ES6 imports statements and bundles only the code which is used. Pretty cool … but … it is a JavaScript bundler only and there are no plugins for Gulp, my favourite task runner. In this article I will show you, how to integrate Rollup in your Gulp bundling pipeline. Install RollupBest practice is to install Rollup globally: 1npm install --global rollup The Gulp FileStarting point was my gulpfile.js as follows: gulpfile.js1234567891011121314151617181920212223242526const { src, dest, watch, series, parallel } = require('gulp');const del = require('del');const cssimport = require("gulp-cssimport");const cleancss = require('gulp-clean-css');const sourcemaps = require('gulp-sourcemaps');/* Clean distribution folder */function clean() { return del('./dist/**', { force: true });}/* Bundle CSS with sourcemapping, imports and cleaning */function css() { return src('./styles/app.css') .pipe(sourcemaps.init()) .pipe(cssimport({})) .pipe(cleancss({ debug: true }, (details) => { console.log(`${details.name} BEFORE: ${details.stats.originalSize}`); console.log(`${details.name} AFTER: ${details.stats.minifiedSize}`); })) .pipe(sourcemaps.write('.', { sourceRoot: '/styles' })) .pipe(dest('./dist/'));}exports.default = series(clean, css); This pipeline only bundles CSS yet, when calling gulp in the command line. Calling Rollup for JS bundlingRollup has dozens of parameters to define everything you need, but it also supports a config file, which allows you to configure everything there and run rollup -c only. Very useful on this approach. rollup.config.js12345678export default { input: './js/app.js', output: { file: './dist/app.js', format: 'es', sourcemap: true }}; As there is no Gulp plugin for Rollup, we need to execute Rollup in the Gulp pipeline by command. For this I’ve created a helper in my gulpfile.js, to be able to execute whichever command: gulpfile.js1234567891011let HELPERS = { execute: (command) => { const process = exec(command); process.stdout.on('data', (data) => { console.log(data.toString()); }) process.stderr.on('data', (data) => { console.log(data.toString()); }) process.on('exit', (code) => { console.log('Process exited with code ' + code.toString()); }) return process; }} This helper is used in a Gulp command function to call Rollup: gulpfile.js123function javascript() { return HELPERS.execute('rollup -c');} The last thing I had to do, is to insert the command in the pipeline to run in parallel to the CSS bundling: gulpfile.js1exports.default = series(clean, parallel(css, javascript)); Pretty straightforward, isn’t it? Happy bundling with Rollup and Gulp…","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"SPA","slug":"SPA","permalink":"https://kiko.io/tags/SPA/"},{"name":"Bundling","slug":"Bundling","permalink":"https://kiko.io/tags/Bundling/"}]},{"title":"Automatic Duplicate Image Shadow","subtitle":"Drop an image shadow with the image itself via JavaScript","date":"2021-07-16","updated":"2021-07-16","path":"post/Automatic-Duplicate-Image-Shadow/","permalink":"https://kiko.io/post/Automatic-Duplicate-Image-Shadow/","excerpt":"At the beginning of the year I wrote a post about showing a shadow on an image with the image itself instead of using box-shadow, to make the image appear glass-like. Nice trick, but it would be much easier to have a little script, that does this automatically for all images on a page. In this post I will show you how to achieve this.","keywords":"beginning year wrote post showing shadow image box-shadow make glass-like nice trick easier script automatically images page show achieve","text":"At the beginning of the year I wrote a post about showing a shadow on an image with the image itself instead of using box-shadow, to make the image appear glass-like. Nice trick, but it would be much easier to have a little script, that does this automatically for all images on a page. In this post I will show you how to achieve this. The script is not really rocket science: We just have to surround an image tag…. 1<img src="my-image.jpg" /> … with a wrapper, which holds the original image and a blurred duplicate of it: 1234<div class="shadow-wrapper"> <img class="drop-shadow" src="my-image.jpg" /> <img class="shadow" src="my-image.jpg" /></div> The rest will be done by CSS: 123456789101112131415161718192021222324div.shadow-wrapper { /* Wrapper */ position: relative; margin-bottom: 30px;}div.shadow-wrapper img.drop-shadow { /* Original image */ position: absolute; left: 0; top: 0; width: 100%; z-index: 1; margin: 0; float: none;}div.shadow-wrapper img.shadow { /* Shadow image */ position: absolute; width: 90%; left: 5%; top: 15%; z-index: 0; filter: blur(10px); opacity: 0.8; margin: 0; float: none;} To make long story short: The wrapper is positioned relative and both images absolute. The shadow image is 10% smaller than the original, blurred by 10 pixels and lies beneath the original one, slightly shifted down by 15%. Very important is to give the wrapper a bottom margin, otherwise the shadow will be rendered to tight to other elements, what looks not good. The ScriptWhat the script should do: Take any image tag with a particular class name (here drop-shadow) Create a new wrapper with all classes the image tag has Size the wrapper as the image Place the image tag inside the wrapper Clone the image tag and append it to the wrapper also Replace the image tag with the new wrapper HTML 1234567891011121314151617181920document.querySelectorAll("img.drop-shadow").forEach(function(item) { let wrapper = document.createElement("div"); wrapper.classList.add("shadow-wrapper"); item.classList.forEach(function(c) { if (c != "drop-shadow") wrapper.classList.add(c); }); wrapper.style.width = item.clientWidth + "px"; wrapper.style.height = item.clientHeight + "px"; wrapper.insertAdjacentHTML("beforeend", item.outerHTML); let shadow = item.cloneNode(); shadow.classList.remove("drop-shadow"); shadow.classList.add("shadow"); wrapper.insertAdjacentHTML("beforeend", shadow.outerHTML); item.outerHTML = wrapper.outerHTML;} The result in comparison with no shadow and the default box-shadow: Happy shadowing…","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"},{"name":"DOM","slug":"DOM","permalink":"https://kiko.io/tags/DOM/"}]},{"title":"Generate Social Media Images Automatically","subtitle":null,"date":"2021-07-10","updated":"2021-07-10","path":"post/Generate-Social-Media-Images-Automatically/","permalink":"https://kiko.io/post/Generate-Social-Media-Images-Automatically/","excerpt":"From day one of this blog I wanted to combine two of my passions: tech stuff and photography. All these photos I have shot myself in recent years and now they are representing my thoughts & findings about digital technology. I wrote about my approach to provide these images in my post Automatic Header Images in Hexo. When I share one of my posts on social media I provide the appropriate image as a visual anchor to my writing. The technique behind this are the meta tags in the HTML of my posts: 123456789<!-- Schema.org for Google --><meta itemprop="image" content="https://kiko.io/photos/normal/DSC_6776.jpg"><!-- Open Graph --><meta property="og:image" content="https://kiko.io/photos/normal/DSC_6776.jpg"><!-- Twitter --><meta property="twitter:card" content="summary_large_image"><meta property="twitter:image" content="https://kiko.io/photos/normal/DSC_6776.jpg"> There are several meta tags for different purposes regarding images. For more information see the links at the end of this post. To make a long story short: The sum of these approaches ensures that when an article is posted, the corresponding image is also displayed in the social media post. But … it’s only the image, without a visual reference to the post itself. In this article I want to show you how to combine the photo with some meta information of the post automatically, to get a Social Media Image.","keywords":"day blog wanted combine passions tech stuff photography photos shot recent years representing thoughts & findings digital technology wrote approach provide images post automatic header hexo share posts social media image visual anchor writing technique meta tags html 123456789<-- schemaorg google --><meta itemprop="image" content="https://kiko.io/photos/normal/DSC_6776.jpg"><!-- open graph property="ogimage" twitter property="twittercard" content="summary_large_image"><meta property="twitterimage" content="https://kiko.io/photos/normal/DSC_6776.jpg"> purposes information links end make long story short sum approaches ensures article posted displayed … reference show photo automatically","text":"From day one of this blog I wanted to combine two of my passions: tech stuff and photography. All these photos I have shot myself in recent years and now they are representing my thoughts & findings about digital technology. I wrote about my approach to provide these images in my post Automatic Header Images in Hexo. When I share one of my posts on social media I provide the appropriate image as a visual anchor to my writing. The technique behind this are the meta tags in the HTML of my posts: 123456789<!-- Schema.org for Google --><meta itemprop="image" content="https://kiko.io/photos/normal/DSC_6776.jpg"><!-- Open Graph --><meta property="og:image" content="https://kiko.io/photos/normal/DSC_6776.jpg"><!-- Twitter --><meta property="twitter:card" content="summary_large_image"><meta property="twitter:image" content="https://kiko.io/photos/normal/DSC_6776.jpg"> There are several meta tags for different purposes regarding images. For more information see the links at the end of this post. To make a long story short: The sum of these approaches ensures that when an article is posted, the corresponding image is also displayed in the social media post. But … it’s only the image, without a visual reference to the post itself. In this article I want to show you how to combine the photo with some meta information of the post automatically, to get a Social Media Image. Starting point of my thoughts were two posts from Drew McLellan (Dynamic Social Sharing Images) and Ryan Filler (Automatic Social Share Images), to which I have already referred in my post Discoveries #11. Drew and Ryan utilizes the Node.JS library Puppeteer, which runs a headless Chromium (or Chrome browser) over the DevTools protocol to process a web page … for example to take a screenshot: 12345678const puppeteer = require('puppeteer');(async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com'); await page.screenshot({ path: 'example.png' }); await browser.close();})(); The idea is, to create a temporary HTML page with the photo and all necessary text for the social media image, take a screenshot of it and save it as PNG. As I run my blog with Hexo, a Static Site Generator (SSG), all information about a post is defined in a Markdown (MD) file with some Frontmatter for the meta information. Therefore, the Social Media Image Generator script in my mind had to do following tasks: Iterate recursively over all MD files in Hexo _source/posts folder Read the MD’s Frontmatter (for information about photo, title, subtitle and more) Create a temporary HTML file with the aid of a template Run Puppeteer script over the temporary file to take a screenshot Store the PNG to a central folder Optimize the PNG Change the meta tags in the posts to reference the new image The FrontmatterI pimped the Frontmatter of the original Hexo configuration a bit, in order to provide an individual photo for each post: 123456789101112---title: Generate Social Media Images Automaticallysubtitle:date: 2021-07-10 11:07:31photograph: file: DSC_6776.jpg name: Color Brushes link: 'https://500px.com/photo/79965349'categories: - JavaScript...--- Among other, there are the basic information, I wanted to have on my social media image: photograph.file (as the image itself) and title, subtitle and categories (for the text on the image). The ScriptThe complete script, in two versions (CommonJS and ES Module) is available at GitHub. tl;dr My script became a JavaScript class, separating the tasks in several methods and a constructor to get all necessary information as parameters. The class exports the main method generate() for calling the script: social-media-image-generator.cjs12345678910111213141516171819202122232425const _currentPath = __dirname;var _postFolder;var _photoFolder;var _templateFile;var _targetFolder;class Generator { constructor(postFolder, photoFolder, templateFile, targetFolder) { _postFolder = path.join(_currentPath, postFolder); _photoFolder = path.join(_currentPath, photoFolder); _templateFile = path.join(_currentPath, templateFile); _targetFolder = path.join(_currentPath, targetFolder); } generate() { ... } getPostFiles(dirPath, allFiles) { ... } async processPost(fileName, vars) { ... } async createImage(fileName, tempFile) { ... }}module.exports.Generator = Generator I chose parameters, in order not to bind the script too tightly to my favourite SSG Hexo: _postFolder - Where are the post files stored? _photoFolder - Where are the photos stored? _templateFile - Where is the template file for the temporary HTML stored? _targetFolder - Where should the generated PNG files be stored? Get the postsFirst task was to get all MD files out of the _postFolder recursively: social-media-image-generator.cjs123456789101112131415161718192021222324252627282930const fs = require("fs");const path = require("path");class Generator { generate() { const postFiles = this.getPostFiles(_postFolder); } getPostFiles(dirPath, allFiles) { // READ FOLDER CONTENT let files = fs.readdirSync(dirPath); //INIT TEMP ARRAY allFiles = allFiles || []; files.forEach((file) => { if (fs.statSync(dirPath + "/" + file).isDirectory()) { // CALL THE METHOD RECURSIVELY allFiles = this.getPostFiles(dirPath + "/" + file, allFiles) } else if (file.indexOf(".md")>=0) { // PUSH MD FILES TO TEMP ARRAY allFiles.push(path.join(dirPath, "/", file)) } }); return allFiles; } }} Get the template and the temporary folderI chose Handlebars as the template engine to generate the temporary HTML file, because it is so easy to handle. social-media-image.handlebars1234567891011121314151617181920212223242526<html> <head> <style> ... </style> </head> <body> <div class="wrap"> ... <img id="photo" src="{{photo}}"> <div class="container"> ... <section id="title"> {{#each categories}} <small>{{this}}</small> {{/each}} <h1>{{title}}</h1> {{#if subtitle}} <h2>{{subtitle}}</h2> {{/if}} </section> </div> </div> </body></html>... Handlebars is able to compile a template into a JavaScript variable, what makes it easy to reuse it. Good for performance and stability. As I wanted to utilize the template to generate temporary HTML files, I needed a temporary folder, which can be deleted afterwards. social-media-image-generator.cjs123456789101112131415161718192021222324const handlebars = require("handlebars");var _template;const _tempFolder = "./~temp";class Generator { constructor(postFolder, photoFolder, templateFile, targetFolder) { ... // GET THE TEMPLATE CONTENT let source = fs.readFileSync(_templateFile).toString('utf8'); // COMPILE THE TEMPLATE FOR FURTHER USE ONCE _template = handlebars.compile(source); // CREATE TEMP FOLDER IN THE WORKING DIRECTORY if (!fs.existsSync(_tempFolder)) { fs.mkdirSync(_tempFolder); } }} Process the postsSecond step was to process all the posts found. social-media-image-generator.cjs1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374const fs = require("fs");const path = require("path");const url = require("url");const frontmatter = require("front-matter");const _tempFolder = "./~temp";class Generator { generate() { let self = this; const postFiles = this.getPostFiles(_postFolder); var postsProcessed = 0; // ITERATE OVER ALL POSTS postFiles.forEach((file) => { fs.readFile(file, 'utf8', function(err, data) { if (err) throw err // READ THE FRONTMATTER let content = frontmatter(data); let fileName = path.basename(file, path.extname(file)); // only process posts with defined photograph file // and if social media file is missing if (content.attributes.photograph?.file && !fs.existsSync(path.join(_targetFolder, fileName + ".png"))) { // CALL PROCESSING METHOD self.processPost( fileName, { title: content.attributes.title, subtitle: content.attributes.subtitle, categories: content.attributes.categories, photo: url.pathToFileURL( path.join(_photoFolder, content.attributes.photograph.file) ) }) .then(() => { // DELETE TEMP FOLDER AFTER PROCESSING if (postsProcessed === postFiles.length) { fs.rmdirSync(_tempFolder, { recursive: true }); } }); } postsProcessed += 1; }) }); } async processPost(fileName, vars) { // GET HTML FOR POST VIA HANDLEBARS let html = _template(vars); let tempFile = path.join(_tempFolder, fileName + ".html"); //WRITE TEMPORARY HTML FILE fs.writeFile(tempFile, html, (err) => { if(err) { throw(err); } //console.log(tempFile + " saved"); }); //CALL IMAGING METHOD await this.createImage(fileName, tempFile); return; }} Get the imageAs I had the temporary HTML file now, I only had to open up a Puppeteer instance, load the file and take the screenshot: 12345678910111213141516171819202122232425262728293031323334353637383940414243const puppeteer = require("puppeteer/cjs-entry");const imagemin = require("imagemin");const imageminPngquant = require("imagemin-pngquant");class Generator { async createImage(fileName, tempFile) { var self = this; // LAUNCH CHROMIUM AND A NEW PAGE const browser = await puppeteer.launch(); const page = await browser.newPage(); // LOAD THE TEMPORARY HTML FILE await page.goto(url.pathToFileURL(tempFile)); // SET THE EXACT WIDTH & HEIGHT await page.setViewport({ width: 1200, height: 630, deviceScaleFactor: 1 }); let imgFile = path.join(_targetFolder, fileName + ".png"); // TAKE SCREENSHOT INTO PNG FILE AT TARGET FOLDER await page.screenshot({ path: imgFile }); await browser.close(); // OPTIMIZE THE PNG FILE await imagemin([imgFile], 'build', { plugins: [ imageminPngquant({ quality: '75-90' }) ] }); return; }} Running the scriptIf you already have lots of post in MD files and appropriate photographs, you can create an execution script… run-social-media-images.cjs123456789const Generator = require("./social-media-image-generator.cjs").Generator;const postFolder = process.argv[2].toString();const photoFolder = process.argv[3].toString();const templateFile = process.argv[4].toString();const targetFolder = process.argv[5].toString();const generator = new Generator(postFolder, photoFolder, templateFile, targetFolder);generator.generate(); … and run it as follows: Example execution in the console...1node "./lib/run-social-media-images.cjs" "../source/_posts" "../static/photos/normal" "../templates/social-media-image.handlebars" "../static/images/social-media" Hexo IntegrationIn case you are running your blog with Hexo also, you can hook on the ready event to let it run on hexo generate automatically: /scripts/on-ready-generate-social-media-images.js1234567891011121314151617181920const log = require('hexo-log')({ debug: false, silent: false});const Generator = require("../lib/social-media-image-generator.cjs").Generator;hexo.on("ready", function() { log.info("Running Social-Media-Image-Generator..."); const postFolder = "../source/_posts"; const photoFolder = "../static/photos/normal"; const templateFile = "../templates/social-media-image.handlebars"; const targetFolder = "../static/images/social-media"; const generator = new Generator(postFolder, photoFolder, templateFile, targetFolder); generator.generate();}); It is important not to store the social-media-image-generator.cjs in Hexo’s scripts folder like the event script above, because Hexo will try to execute it automatically. You have to create a different folder like lib to store and reference it from there. The ResultHere is the result from my approach in Hexo, as I run hexo generate for this blog post: The very last thing I had to do, was to change the source of the image meta tag mentioned at the top, to reference to newly created social media image. Here’s the new image in action at Twitter: More Info CSS Tricks: The Essential Meta Tags for Social MediaThe Open Graph Protocol: The Open Graph ProtocolDrew McLellan: Dynamic Social Sharing ImagesRyan Filler: Automatic Social Share ImagesThe GitHub Blog: A framework for building Open Graph images","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"},{"name":"ES6","slug":"ES6","permalink":"https://kiko.io/tags/ES6/"}]},{"title":"Discoveries #11","subtitle":null,"series":"Discoveries","date":"2021-06-28","updated":"2021-06-28","path":"post/Discoveries-11/","permalink":"https://kiko.io/post/Discoveries-11/","excerpt":"Todays issue of Discoveries is all about images. Show them, manipulate them, get information about them, protect them, use them and create them with JavaScript. Have fun trying out one of these cool solutions. PhotoSwipelightGalleryPicaResemble.jsmedium-zoomLC-Mouse-DragProtectImage.jsTinygraphsDynamic Social Sharing ImagesAutomatic Social Share Images","keywords":"todays issue discoveries images show manipulate information protect create javascript fun cool solutions photoswipelightgallerypicaresemblejsmedium-zoomlc-mouse-dragprotectimagejstinygraphsdynamic social sharing imagesautomatic share","text":"Todays issue of Discoveries is all about images. Show them, manipulate them, get information about them, protect them, use them and create them with JavaScript. Have fun trying out one of these cool solutions. PhotoSwipelightGalleryPicaResemble.jsmedium-zoomLC-Mouse-DragProtectImage.jsTinygraphsDynamic Social Sharing ImagesAutomatic Social Share Images PhotoSwipe by Dmytro Semenov https://photoswipe.com PhotoSwipe is a simple, but powerful image gallery written with jQuery, which has plugin support for some CRM’s. Currently Dmytro is working on the next version v5. lightGallery by Sachin Neravath https://www.lightgalleryjs.com/ Another wionderful image gallery, but with more (useful?) images features, realized as plugins. It supports video and IFrames also and is fully responsive. Pica by Nodeca https://github.com/nodeca/pica Pica is an Open Source project for resizing images with JavaScript. It supports several technologies, like WebAssembly, WebWorkers or even pure JS. Resemble.js by James Cryer http://rsmbl.github.io/Resemble.js/ Analyse and compare images with Javascript and visualizes the differences. Great stuff. medium-zoom by François Chalifour https://github.com/francoischalifour/medium-zoom Zoom your images on your website to fullsize like on medium.com. LC-Mouse-Drag by Luca from LCWeb Italia https://github.com/LCweb-ita/LC-Mouse-Drag Instead of zooming your image on your website, you can let the users scroll into an oversized image, to see the details. ProtectImage.js by ColonelParrot https://github.com/ColonelParrot/ProtectImage.js ProtectImage.js is a Javascript library that helps prevent image theft by disabling traditional user interactions to download/copy images. Tinygraphs by Taironas https://www.tinygraphs.com/ Supporting avatars in the comment section of a website is cool, but not every user wants to share an image. Tinygraphs helps out with random graphic images. Dynamic Social Sharing Images by Drew McLellan https://24ways.org/2018/dynamic-social-sharing-images/ Sharing posts on Social Media is vital to get readers and it is a good idea to provide a generic post image. Drew shows how to utilize Node.js to get a page screenshot automatically. The posted script has some pitfalls, but I will post a HowTo shortly. Automatic Social Share Images by Ryan Filler https://www.ryanfiller.com/blog/automatic-social-share-images/ Having a screenshot of a page is one side of the coin on creating Social Share images. Ryan shows us, how to enrich them with the appropriate text.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Open Source Insights - Seeing the big picture","subtitle":"Google's new experimental platform to get a grip on project dependencies","series":"Great Finds","date":"2021-06-06","updated":"2021-06-06","path":"post/Open-Source-Insights-Seeing-the-big-picture/","permalink":"https://kiko.io/post/Open-Source-Insights-Seeing-the-big-picture/","excerpt":"A while ago I needed some functionality regarding database access in one of my spare time projects and I decided to use a library from NPM. Typed npm install and the hell was breaking loose … 186 direct dependencies and nearly 200K of files were flooding my harddrive! The mental basis of IT is lazyness, which means that we produce software to make our and others life easier. This also applies to the building process. Don’t reinvent the wheel, but reuse the work of other developers. But … we have to recognize the limits and prevent to fall into the dependency hell. To get a better overview over dependencies, regarding NPM and other repositories, some Google engineers have published a project called Open Source Insights a couple of days ago.","keywords":"ago needed functionality database access spare time projects decided library npm typed install hell breaking loose … direct dependencies 200k files flooding harddrive mental basis lazyness means produce software make life easier applies building process dont reinvent wheel reuse work developers recognize limits prevent fall dependency overview repositories google engineers published project called open source insights couple days","text":"A while ago I needed some functionality regarding database access in one of my spare time projects and I decided to use a library from NPM. Typed npm install and the hell was breaking loose … 186 direct dependencies and nearly 200K of files were flooding my harddrive! The mental basis of IT is lazyness, which means that we produce software to make our and others life easier. This also applies to the building process. Don’t reinvent the wheel, but reuse the work of other developers. But … we have to recognize the limits and prevent to fall into the dependency hell. To get a better overview over dependencies, regarding NPM and other repositories, some Google engineers have published a project called Open Source Insights a couple of days ago. If you look at a library on NPM, you immediately see the direct dependencies, but that could just be the tip of the iceberg. Insights shows you the big picture in form of an interactive graph or a complete list with many additional information of the complete dependency chain. As Google says: Your software and your users rely not only on the code you write, but also on the code your code depends on, the code that code depends on, and so on. An accurate view of the complete dependency graph is critical to understanding the state of your project. Next time, before you type npm install ... take a quick look at the Insights page of the library, because it won’t give you an overview over the dependecies only, but also some security advisories to let you decide if it is a good idea to use it. Very valueable for the authors of exisiting NPM projects also, because it shows the consequences of dependency decisions very clearly and gives hints to make the own library less sensitive to vulnerabilities. Let us hope, that Google removes the describing adjective experimental from the project some day and adds more and more functionality to it (like a working responsiveness ;) in order to establish Open Source Insights as a main source of information about reusable Open Source software.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Node.js","slug":"Node-js","permalink":"https://kiko.io/tags/Node-js/"}]},{"title":"Discoveries #10","subtitle":null,"series":"Discoveries","date":"2021-05-24","updated":"2021-05-24","path":"post/Discoveries-10/","permalink":"https://kiko.io/post/Discoveries-10/","excerpt":"Todays Discoveries it’s all about my favourite programming language JavaScript. Some tiny tips and tricks alongside with a deep dive into ‘console’ and some helful UI libraries. Have fun… Beyond Console.log()DOMGuard - Stop scammers from the manipulating DOMHandling User Permissions in JavaScripthtml-chain - Make html by chaining javascript functionsAccessible AutocompleteJS DataTableMK ChartsSnabbt.js - Fast animations with Javascript and CSS transformsSimplyLazy - Pure JavaScript Image Lazy LoaderBlury-Loading","keywords":"todays discoveries favourite programming language javascript tiny tips tricks alongside deep dive console helful ui libraries fun… consolelogdomguard - stop scammers manipulating domhandling user permissions javascripthtml-chain make html chaining functionsaccessible autocompletejs datatablemk chartssnabbtjs fast animations css transformssimplylazy pure image lazy loaderblury-loading","text":"Todays Discoveries it’s all about my favourite programming language JavaScript. Some tiny tips and tricks alongside with a deep dive into ‘console’ and some helful UI libraries. Have fun… Beyond Console.log()DOMGuard - Stop scammers from the manipulating DOMHandling User Permissions in JavaScripthtml-chain - Make html by chaining javascript functionsAccessible AutocompleteJS DataTableMK ChartsSnabbt.js - Fast animations with Javascript and CSS transformsSimplyLazy - Pure JavaScript Image Lazy LoaderBlury-Loading Beyond Console.log() by Christian Heilmann https://www.sitepoint.com/beyond-console-log-level-up-your-debugging-skills/ The browser console is propably the most used tool for debugging JavaScript, but most of the time we all just scatch the surface. Christian show us the power of the console. DOMGuard - Stop scammers from the manipulating DOM by David Wells https://dom-guard.netlify.app/ There are many attack vectors scammers use to draw money out of the pockets. You have to make it as difficult as possible for them. Davids idea is to protect the DOM of the browser against changes utilizing the JS MutationObserver. Clever. Handling User Permissions in JavaScript by Andreas Remdt https://css-tricks.com/handling-user-permissions-in-javascript/ In case you have to intregrate a permission system into your Web App, to separate features from different user groups, Andreas post on CSS Tricks is a very good entry point into the subject. html-chain - Make html by chaining javascript functions by Matthew Elphick https://github.com/maael/html-chain There are several ways on dealing with HTML in JavaScript. My favourite approach are literals. Matthew gives us with his library the possibility to do it in a LINQ-style by chaining commands. Accessible Autocomplete by Government Digital Service https://github.com/alphagov/accessible-autocomplete Many cool looking UI elements on the web are not accessible for the impaired. But especially public services has to be aware of that. Developers from the British Government Digital Service have created a full WAI-ARIA compatible library for autocomplete inputs. JS DataTable by Luigi Verolla https://github.com/luverolla/js-datatable Deaing with tables in HTML can be a mess, when you try to add some functionality like searching, sorting and paging and that also responsive. Take a nap, because Luigi has a fully functional solution for this. MK Charts by Marcus Kirschen https://mkirschen.de/mk-scripts/mk-charts/ Dashboards everywhere. In case you don’t have a specialized UI library and just want to add some circle charts to your UI, try out Marcus’ solution. Just define the values in your HTML tag and let MK Charts do the rest. Simple and easy. Snabbt.js - Fast animations with Javascript and CSS transforms by Daniel Lundin https://daniel-lundin.github.io/snabbt.js/ Snabbt is quite old in terms of the IT industry, but still worth mentioning, because it is a really light and fast solution for adding animations to your Web App. See the demos … it’s still stunning. SimplyLazy - Pure JavaScript Image Lazy Loader by Max (maxshuty) https://maxshuty.github.io/simply-lazy/ Lazy loading can be a must on image heavy webs and you got bazillion results while searching for the right JS library. I can recommend Max’s solution, because it’s quite tiny and has callback as well as default image support. Blury-Loading by S.M.Abtahi Noor https://github.com/19smabtahinoor/Blury-Loading Apropos loading … maybe you want to preload your Web App’s sources completely and show the user a loading visual? Take this nice looking approach: while a percentage figure is running upwards, the background image is getting less blurry. A three-liner, but cool. Thanks Mr. Noor.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Hexo and the IndieWeb (Receiving Webmentions)","subtitle":"Use webmention.io the easy way","series":"IndieWeb","date":"2021-05-13","updated":"2021-05-13","path":"post/Hexo-and-the-IndieWeb-Receiving-Webmentions/","permalink":"https://kiko.io/post/Hexo-and-the-IndieWeb-Receiving-Webmentions/","excerpt":"This is part three of the splitted original post Hexo and the IndieWeb. Don’t miss Part 2 Hexo and the IndieWeb (Sending Webmentions) either. A meaningful interaction has always two directions: sending and receiving. In this part of the post I want to show you how to receive Webmentions from other blogs participating in the IndieWeb. As Hexo is a SSG it generates static HTML pages. This has the advantage that the pages can be hosted just about anywhere (in my case Github Pages, but also the disadvantage of not having a real backend. Therefore, we need an external service that acts as an Webmention endpoint, where other people can send their webmentions. Aaron Parecki, co-founder of the IndieWeb, has made a service called webmention.io we can use for free. It is able to convert old-fashioned Pingbacks to Webmentions, supports deleting of unwanted mentions, has a Blocklist for blocking domains, Webhooks for real-time processing and last but not least an API to get all your Webmentions per page or per site.","keywords":"part splitted original post hexo indieweb dont miss sending webmentions meaningful interaction directions receiving show receive blogs participating ssg generates static html pages advantage hosted case github disadvantage real backend external service acts webmention endpoint people send aaron parecki co-founder made called webmentionio free convert old-fashioned pingbacks supports deleting unwanted mentions blocklist blocking domains webhooks real-time processing api page site","text":"This is part three of the splitted original post Hexo and the IndieWeb. Don’t miss Part 2 Hexo and the IndieWeb (Sending Webmentions) either. A meaningful interaction has always two directions: sending and receiving. In this part of the post I want to show you how to receive Webmentions from other blogs participating in the IndieWeb. As Hexo is a SSG it generates static HTML pages. This has the advantage that the pages can be hosted just about anywhere (in my case Github Pages, but also the disadvantage of not having a real backend. Therefore, we need an external service that acts as an Webmention endpoint, where other people can send their webmentions. Aaron Parecki, co-founder of the IndieWeb, has made a service called webmention.io we can use for free. It is able to convert old-fashioned Pingbacks to Webmentions, supports deleting of unwanted mentions, has a Blocklist for blocking domains, Webhooks for real-time processing and last but not least an API to get all your Webmentions per page or per site. Setupwebmention.io needs a registration and Aaron uses his own authentication method Web Sign-In over IndieLogin to achieve that. After you have signed in, you see your settings with two important links, you have to integrate in the head of your HTML: 12<link rel="webmention" href="https://webmention.io/[YOUR-BLOG-DOMAIN]/webmention" /><link rel="pingback" href="https://webmention.io/[YOUR-BLOG-DOMAIN]/xmlrpc" /> They define your page on the one hand as a webmention- and on the other hand as a pingback endpoint. Every send Webmention or Pingback from another blog is routed to these URL’s. You will also find your API Key on this page. To be honest, is not a real API key, but rather a key to retrieve all incoming webmentions at once. Therefore it does not need to be kept private and secured. There is no way to modify your incoming Webmentions over it. Incoming Webmentions I’m implementing my Hexo/Webmention solution as I’m writing this post, therefore I don’t have any Webmentions to show as an example. Since all mentions on webmention.io are publicly accessible, I simply use Max Böcks article Using Webmentions in Eleventy in the following. Max, I hope it is ok … :) Aaron has defined three ways to get the incoming mentions of a particular article on your blog: 1. View as HTML page https://webmention.io/api/mentions.html?target=https://mxb.dev/blog/using-webmentions-on-static-sites/ Using it this way, webmention.io integrates the content with some styles and other information of the sending post for easy reading. 2. Consume as Atom feed https://webmention.io/api/mentions.atom?target=https://mxb.dev/blog/using-webmentions-on-static-sites/ 3. Get as JSON https://webmention.io/api/mentions.jf2?target=https://mxb.dev/blog/using-webmentions-on-static-sites/ Especially the last one is interesting for us, because we can use it to automate getting the data and integrate it in our blog post. You can also use all these Url’s to get the Webmentions of all of your pages, by changing the target parameter into: ...?token=[YOUR-API-KEY] The feed with token for getting all webmentions is particularly practical for checking whether there are new ones for your blog via a feed reader. There are some parameters you have to be aware of: Parameter Example Paging ?per-page=20&page=0 (default, page 1 with 20 entries) Sorting By ?sort-by=created (default) Sorting Direction ?sort-dir=down (default, newest first) Time Limit ?since=2021-05-10T12:00:00-0700 ID Limit ?since_id=500). You can find more on parameters in the documentation of webmention.io’s source code here: https://github.com/aaronpk/webmention.io. The DataLet’s dive into the data. The JSON is a list and every Webmention is an entry underneath children, with following useful fields: Field Purpose author.name Name of the sender author.photo Avatar photo of the sender author.url Personal URL of the sender wm-id Unique ID of the Webmention wm-target URL of your post wm-received UTC Date/Time sended wm-source URL of the sending post published Publish Date/Time of the sending post wm-property Webmention type out of the following values:mention-of : Mention as link in postin-reply-to : Reply to your postlike-of : Like of your postrepost-of : Repost of your postbookmark-of : Bookmark of your post Other occuring fields are optional, but no less interesting for displaying them at your post: Field Purpose name Name of the Webmention (title of the sending post or something) summary.type Summary type of the Webmention, e.g. “text/plain” summary.value Summary text of the Webmention content.html Content as HTML of sending post content.text Content as text of sending post The fact that we get the complete HTML content of the sending post, means that we easily can parse it for IndieWeb microformats (see Part 1) to get even more information about it and the blog owner! IntegrationWe have two ways to choose from in order to integrate the data thus obtained into the post: Static … means while generating the site Dynamic … means real-time via client-side Javascript The static approach has two big disadvantages: The Webmentions are only visible with a time delay, due to the need of generating the pages of the site Hexos generation mechanism only takes those, whose content has beed updated in the meanwhile, but do not take into account any external data like Webmentions. We would have to fire up the time consuming clean and generate all the time So, let’s go for the dynamic approach, a little bit JavaScript, which loads the Webmentions on page load (like most Commenting platforms do). First of all, we need a placeholder in our article.ejs, where the Webmentions will be displayed. Somewhere near the comments and implemented as another partial: layout/_partial/article.ejs12345678910111213<article id="<%= post.layout %>-<%= post.slug %>" class="article article-type-<%= post.layout %> h-entry" itemscope itemprop="blogPost"> ... <%- partial('post/webmentions') %> <%- partial('post/comments') %> <%- partial('post/related') %> <%- partial('post/nav') %></article> layout/_partial/post/webmentions.ejs12345678910111213<div class="article-webmentions"> <div class="webmentions-placeholder"> <p class="wm-placeholder">No Webmentions yet...</p> </div> <script> window.addEventListener('load', function () { insertWebmentions('<%- post.slug.toLowerCase() %>'); }) </script></div> The call insertWebmentions references to the asset script webmentions.js, which will be bundled via Grunt in my case, but you can place it where you want and add it to your articles head element. The main thing is, that it will load when the article page is launched. The script checks, if the user has already loaded the Webmentions for the current page from webmention.io in the last hour and do so if not, and stores the data in the browser by the pages key (slug) and with a timestamp: assets/webmentions.js12345678910111213141516171819202122232425262728293031323334353637383940414243function insertWebmentions(key) { const lsTimestamp = "wmts_" + key; const lsWebmentions = "wm_" + key; const currentUrl = window.location.href; const wmUrl = `https://webmention.io/api/mentions.jf2?target=${currentUrl}&per-page=1000&sort-dir=up`; let lastRequest; let webmentions; // Get data from browser storage, if available if (localStorage.getItem(lsTimestamp) && localStorage.getItem(lsWebmentions)) { lastRequest = localStorage.getItem(lsTimestamp); webmentions = JSON.parse(localStorage.getItem(lsWebmentions)); } if(webmentions && lastRequest && Math.abs(Date.now() - lastRequest) / (60*60*1000) < 1) { // Webmentions are present and not older than an hour process(); } else { // Get Webmentions from webmention.io load().then(() => { process(); }); }; /** * Load webmention.io's JSON data for the current page */ async function load() { const response = await fetch(wmUrl); webmentions = await response.json(); localStorage.setItem(lsWebmentions, JSON.stringify(webmentions)); localStorage.setItem(lsTimestamp, Date.now()); } /** * Process Webmentions */ function process() { ... }} Actually the script loads the first 1000 Webmentions in ascending order of their retreiving date. Should last for a while or I may feel like adding real paging at some point ;) On processing the webmentions, a separate HTML block is generated for each type (wm-property) regarding the content of the Webmention, whereby the header is always the same. The VERB refers to the Webmentions type: mentioned, replied, liked, bookmarked or reposted. HEADER1234567891011121314<div class="wm-card h_card"> <a class="wm-photo-link u-url" href="[AUTHOR.URL]"> <img class="wm-photo u-photo" width="44" height="44" src="[AUTHOR.PHOTO]" alt="[AUTHOR.NAME]"> </a> <div class="wm-meta"> <a class="wm-name p-name" href="[AUTHOR.URL]">[AUTHOR.NAME]</a> <span class="wm-verb">[VERB] on</span> <time class="wm-date dt-published" datetime="[WM-RECEIVED]"> [formatted WM-RECEIVED] </time> <small>[Running Number]</small> </div></div> The HTML of the five implemented types: MENTION1234567<div class="webmention wm-mentioned" id="[WM-ID]"> [HEADER HTML] <div class="wm-content p-content"> <p>[First 50 words of CONTENT.TEXT with ellipsis]</p> <a class="wm-source" href="[WM-SOURCE]">[WM-SOURCE]</a> </div></div> REPLY1234567<div class="webmention wm-mentioned" id="[WM-ID]"> [HEADER HTML] <div class="wm-content p-content"> <p>[Complete CONTENT.HTML]</p> <a class="wm-source" href="[WM-SOURCE]">[WM-SOURCE]</a> </div></div> LIKE & BOOKMARK (no content)123<div class="webmention wm-mentioned" id="[WM-ID]"> [HEADER HTML]</div> REPOST12345678<div class="webmention wm-mentioned" id="[WM-ID]"> [HEADER HTML] <div class="wm-content p-content"> <p> ... at <a href="[WM-SOURCE]">[WM-SOURCE]</a> </p> </div></div> The result will look like this, after adding some styles: You can download the complete JavaScript- and the Stylus file on Github:https://github.com/kristofzerbe/Hexo-and-the-IndieWeb-Files Form for sending Webmentions manuallyAs you may noticed in Part 2, some manual work is necessary to have webmentions sent. Therefore, it is good to give the user a possibility to submit his blog post, where he mentions yours, directly. As webmentions.io supports posting a new source, the HTML form is quite simple an we can integrate it in the partial from above, below the script tag: layout/_partial/post/webmentions.ejs1234567891011121314151617181920<div class="article-webmentions"> ... <form class="webmention-form" action="https://webmention.io/<%- config.title %>>/webmention" name="webmention-form" method="post"> <label for="webmention-form-source">Your Article URL:</label><br> <input class="webmention-form-source" type="url" name="source" placeholder="https://your-blog.com/your-article" required=""> <input type="hidden" name="target" value="<%- post.permalink %>>"> <input type="submit" value="Send Webmention"> </form></div> The variable config.title defines the name of your blog with which you have registered at webmention.io. In the hidden input, we use the permalink of the current page/article as the mentions target. Building BridgesYes, the IndieWeb and Webmentions are an alternative concept of social networking, bypassing the big silos like Facebook, Twitter and Co. … but they exist and they have a massive reach. Of course, your posts will be shared there, so why not include those mentions? Bridgy is such a bridge builder. It connects your blog with the big social media players: The only thing you have to do, is to allow Bridgy to access your social media data, like your Tweets on Twitter. If somebody mentions you and one of your articles as a Tweet, Reply or Like, it sends a mention to the endpoint defined in your HTML, in our case webmention.io. Thats it … kinda magic. SummarySome people say, Webmentions makes commenting forms on blogs, like Disqus or others, obsolete, but I don’t agree with that, because not every visitor owns a blog and not every blog owner want’s to write a post only to comment the thoughts of another blogger. Both approaches work well side by side and complement each other. Webmentions are super to build a blog network and increasing your blogs coverage. As I said in Part 1 … we write for readers. More Info indieweb.org: webmention.ioMax Böck: Using Webmentions in EleventyMax Böck: Webmention AnalyticsPaul Kinlan: Using Web Mentions in a static site (Hugo)Sia Karamalegos: An In-Depth Tutorial of Webmentions + EleventyKeith J. Grant: Adding Webmention Support to a Static SiteChris Bongers: Goodbye comments, welcome Webmentions","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Share","slug":"Share","permalink":"https://kiko.io/tags/Share/"}]},{"title":"Hexo and the IndieWeb (Sending Webmentions)","subtitle":"Use webmention.app the easy way","series":"IndieWeb","date":"2021-05-08","updated":"2021-05-08","path":"post/Hexo-and-the-IndieWeb-Sending-Webmentions/","permalink":"https://kiko.io/post/Hexo-and-the-IndieWeb-Sending-Webmentions/","excerpt":"This is part two of a blog post that turned out to be a bit too long. Don’t miss Part 1: Hexo and the IndieWeb … After you have created your new Hexo post with hexo new post "My Fancy Post" and spend a couple of minutes/hours/days on writing meaningful text, you publish it by running hexo generate and copying the generated HTML to your server. Next step would be to inform all the blogs you linked to in your now published post, that you have done just that. You want to send Webmentions. Good news: you don’t have to write your own solution to scan your article for external URL’s and sending Webmentions to their creators: Remy Sharp has done that already with his service webmention.app. It supports the long existing Pingbacks too and offers several ways to achieve your goal:","keywords":"part blog post turned bit long dont miss hexo indieweb … created "my fancy post" spend couple minutes/hoursys writing meaningful text publish running generate copying generated html server step inform blogs linked published send webmentions good news write solution scan article external urls sending creators remy sharp service webmentionapp supports existing pingbacks offers ways achieve goal","text":"This is part two of a blog post that turned out to be a bit too long. Don’t miss Part 1: Hexo and the IndieWeb … After you have created your new Hexo post with hexo new post "My Fancy Post" and spend a couple of minutes/hours/days on writing meaningful text, you publish it by running hexo generate and copying the generated HTML to your server. Next step would be to inform all the blogs you linked to in your now published post, that you have done just that. You want to send Webmentions. Good news: you don’t have to write your own solution to scan your article for external URL’s and sending Webmentions to their creators: Remy Sharp has done that already with his service webmention.app. It supports the long existing Pingbacks too and offers several ways to achieve your goal: (1) Use the service at https://webmention.app/test manually with your newly published article URLVery time consuming approach and actually only intended for tests. You rely on Remy’s service and need a token to avoid existing rate limits. (2) Perform a POST of your new article URL or the URL of your RSS feed to his service endpoint using CURL or any other HTTP requesting methodCan be integrated at the end of your build & deploy process. Rely on Remy’s service and needs a token. (3) Use IFTTT to do the job described in (2) using your feedSeems best way for ‘set up and forget’, but you rely not only on Remy’s service, but on IFTTT also. Not really controllable. (4) Use his Command line tool independentlyUse the executable behind Remy’s service on your own, without tokens and stuff. Integrable into your build & deploy process and the source code is available on Github, in case you want to change it to your needs. 12345678REM Installnpm install @remy/webmentionREM Run test locallynpx webmention https://localhost:4000/my-fancy-post --debugREM Run send with your published URLnpx webmention https://my-blog.com/my-fancy-post --send --limit 0 Whatever method you use: If you use your RSS feed for scanning, you have to ensure, that your feeds content contains the complete text of your article and not only the excerpt, in case you work with Hexo’s <!-- more --> feature. Sending Hexo-StyleTo avoid having to look up the published URL each time, I wrote a Hexo console command that either processes a post by its filename (slug) or simply the last ones. It is based on and uses Remy’s NPM package. You can install hexo-console-webmention by executing: npm install hexo-console-webmention --save The command has 3 options: Parameter Type Description slug string Parse a particular post by its filename (slug) count int Parse a number of latest posts (not considered when –slug is used)>; default = 1 send bool Parse and send Webmentions (without, only the endpoints found are displayed) ExamplesParse and show endpoints for the latest post: 1hexo webmention Parse and show endpoints for the latest 20 posts: 1hexo webmention --count 20 Parse and send Webmentions for the latest post: 1hexo webmention --send true Parse and send Webmentions for the post “My Fancy Blogpost”: 1hexo webmention --slug my-fancy-blogpost --send true Sample Output The source code is available at: https://github.com/kristofzerbe/hexo-console-webmention. Do not linger … this way to Part 3: Hexo and the IndieWeb (Receiving Webmentions) … More Info Paul Kinlan: Webmention.appRemy Sharp: Send Outgoing WebmentionsGithub: Remy Sharp: Source Code from webmention.app","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Share","slug":"Share","permalink":"https://kiko.io/tags/Share/"}]},{"title":"Hexo and the IndieWeb","subtitle":"Make your blog ready for social interaction via Webmentions","series":"IndieWeb","date":"2021-05-05","updated":"2021-05-05","path":"post/Hexo-and-the-IndieWeb/","permalink":"https://kiko.io/post/Hexo-and-the-IndieWeb/","excerpt":"Posted on IndieNews It is cool to publish your thoughts on your own blog under your only domain and not only on big social media platforms, because that way you keep control over your content. But what makes Facebook, Twitter and others “social” is the interaction between the people. Likes, Retweets, Mentions, Replies are the fuel which drives them. But most of the blogging solutions offers only rudimentary interactions, in form of article comments. The comment hurdle is high because interacting on someone else’s site is different from interacting on what is supposed to be your own, such as your Twitter or Facebook feed. The project IndieWeb and their approach of Webmentions, has the goal to fill this gap. As a W3C recommendation, it defines standards how the social interaction of independent blogging solutions can be technically implemented without the need of manual intervention. Let software do the job… In this article I will only briefly go into the basics and then show an implementation solution for the SSG Hexo.","keywords":"posted indienews cool publish thoughts blog domain big social media platforms control content makes facebook twitter interaction people likes retweets mentions replies fuel drives blogging solutions offers rudimentary interactions form article comments comment hurdle high interacting elses site supposed feed project indieweb approach webmentions goal fill gap w3c recommendation defines standards independent technically implemented manual intervention software job… briefly basics show implementation solution ssg hexo","text":"Posted on IndieNews It is cool to publish your thoughts on your own blog under your only domain and not only on big social media platforms, because that way you keep control over your content. But what makes Facebook, Twitter and others “social” is the interaction between the people. Likes, Retweets, Mentions, Replies are the fuel which drives them. But most of the blogging solutions offers only rudimentary interactions, in form of article comments. The comment hurdle is high because interacting on someone else’s site is different from interacting on what is supposed to be your own, such as your Twitter or Facebook feed. The project IndieWeb and their approach of Webmentions, has the goal to fill this gap. As a W3C recommendation, it defines standards how the social interaction of independent blogging solutions can be technically implemented without the need of manual intervention. Let software do the job… In this article I will only briefly go into the basics and then show an implementation solution for the SSG Hexo. Basic ConceptsNothing describes the flow of Webmentions better than this: Frankie posts a blog entry. Alex has thoughts in response, so also posts a blog entry linking to Frankie’s. Alex’s publishing software finds the link and fetches Frankie’s post, finding the URL of Frankie’s Webmention endpoint in the document. Alex’s software sends a notification to the endpoint. Frankie’s software then fetches Alex’s post to verify that it really does link back, and then chooses how to display the reaction alongside Frankie’s post. --- Drew McLellan Basically Webmentions allow notifications between web addresses, therefore every post, which is part of the interaction, has to have a unique permalink. A blog software that wants to support webmentions must cover 4 main points: The HTML has to tell others who you are The HTML has to give dedicated informations about your posts Sending a message to another blog, in case you mentioned one of its posts Reveiving messages from other blogs, in case they mentioned one of your posts Point 4 is probably the most interesting for all of us, because it pats our own ego on the back, since we usually don’t write for ourselves, but for others, and reactions to it, show us that it wasn’t pointless. Step 1: The Personal & Profile HTMLAs you want to interact with other blogs participating in the IndieWeb with your posts, they have to know something about you and your articles in a machine-readable form. Personal InformationHTML is machine-readable per se, but you have to tell others what to look for by adding defined classes to the tags which holds the information, in order to enable them to get specific information about you, like your name, your mail-address or links to other profiles f.e. Github, Twitter and so on. It is necessary to have this information not only in an ABOUT page, but also on each post page. You can achieve this either by having an ABOUT block like here on kiko.io or providing the information in hidden HTML tags elsewhere in your HTML. It does not matter which tags you use, you only have to add the defined class to the tag of a particular information in your Hexo EJS file. The information will be extracted out of the tag’s inner text. The most used classes for personal blogs as follows: Class Information h-card Wrapper for all personal information. All other classes below has to be used on child tags p-name Full name u-email Email address u-photo Photo p-role Role u-url URL representing the person p-locality City or Town p-region State or province p-country-name Country name Please keep in mind not to give too much information about you to the public. It could get unpleasant… Profile InformationFor providing links to other profiles, anchor (A) tags with the special attribute rel="me" will be used, which indicates profile equivalence and can be used for identity-consolidation. With this extension of your blog HTML, you are able to sign in using your domain at sites which supports Web Sign-In over the concept of RelMeAuth, for example those who use IndieAuth.net - OAuth for the open web. You only have to make sure, that the endpoints of your profile links have backlinks to your blog with a rel="me“. Unfortunately, not many services offer the definition of such a backlink. Github, for example, is an exception. You can give it a try at IndieAuth.com. Example Step 2: The Article HTMLTagging articles with meta information for the IndieWeb is similarly simple, by adding following classes in your article.ejs file: Class Information h-entry Wrapper for all article related information p-name Title p-summary Short summary e-content Content dt-published Publish date dt-updated Update date u-url Permalink In case you work with the default Hexo theme ‘landscape’, I advise you to split your article.ejs in two files, because it is used for the article itself and for the excerpts on the start page and archive pages also. I have made an excerpt.ejs with all the information needed for listing the posts and cut back my article.ejs to the bare minimum, but with the IndieWeb related classes above (or in the linked partials if necessary), because only the article page itself should have these informations, respectively an h-entry class, to indicate that there are IndieWeb data! layout/_partial/article.ejs123456789101112131415161718192021222324252627282930313233343536<article id="<%= post.layout %>-<%= post.slug %>" class="article article-type-<%= post.layout %> h-entry" itemscope itemprop="blogPost"> <div class="article-meta"> <div class="h-card p-author" style="display:none"> <img class="u-photo" src="<%- config.photo %>" alt="<%- config.author %>" /> <a class="p-name u-url" href="<%- config.url %>" rel="author"><%- config.author %></a> </div> <%- partial('post/date', { class_name: 'article-date dt-published', date_format: 'DD MMM YYYY' }) %> <%- partial('post/category', { class_name: 'article-category p-category' }) %> </div> <div class="article-inner"> <header class="article-header"> <%- partial('post/title', { class_name: 'article-title p-name', show_link: false }) %> <%- partial('post/subtitle', { class_name: 'article-subtitle p-summary' }) %> </header> <div class="article-entry e-content" itemprop="articleBody"> <%- post.content %> </div> <footer class="article-footer"> <%- partial('post/tag', { class_name: 'article-tags' }) %> <%- partial('post/permalink', { class_name: 'article-permalink u-url' }) %> </footer> </div> <%- partial('post/comments') %> <%- partial('post/related') %> <%- partial('post/nav') %></article> External LinksThe Interaction with other blogs takes place through linking to those external sources in the content of your article. Lets say you want to write about a specific topic and to mention the work of another developer, then you just place a link to his post in your Markdown, as you have been doing all along: /source/_posts/my-fancy-post.md1234# My Fancy Post...Jack has done a wonderful job with his [Awesome Work](https://jacks-blog.com/awesome-work)... It will be transformed while generating into something like that: /output/.../my-fancy-post/index.html123456789101112131415161718192021<body> ... <article class="h-entry"> ... <div class="article-inner"> <header class="article-header"> <h1 class="p-name">My Fancy Post</h1> </header> <div class="article-entry e-content"> ... <p> Jack has done a wonderful job with his <a href="https://jacks-blog.com/awesome-work">Awesome Work</a>. </p> ... </div> ... </div> ... </article> ...</body> In the terms of the IndieWeb concept, your post will a be an article, which mentions other posts, as the old-fashioned pingbacks do. Special Post FormatsA true interaction takes place, when you are posting in a certain syndication context … with a note as a response to the work of others, mainly by adding additional classes to the external link: u-in-reply-to … to indicate that your post is a reply to a post as part of a conversation u-like-of … to indicate that your post is a like u-repost-of … to indicate that your post is a repost (100% re-publication) u-bookmark-of … to indicate that your post is a bookmark Every response type can have additional information about your post and the syndication of it. Example REPLY12345678910111213<body> ... <div class="h-entry"> <p> In reply to: <a class="u-in-reply-to" href="https://jacks-blog.com/awesome-work">Jacks Blog: Awesome Work</a> </p> <p class="p-name e-content"> Jack, you have done a wonderful job! </p> ... </div> ...</body> Example LIKE12345678910<body> ... <div class="h-entry"> <p class="p-summary"> Kristof liked <a class="u-like-of" href="https://jacks-blog.com/awesome-work">Jacks Awesome Work at https://jacks-blog.com/awesome-work</a> </p> ... </div> ...</body> Currently, I would not recommend writing responses as a normal post in Hexo, as it is based on structured text, that best describes the IndieWeb concept of an ARTICLE.As this post is part of a new series called IndieWeb, I will post a solution for responses is the near future. VerificationTo check all your changes, you can use IndieWebify.Me (Level 1 & 2): This was supposed to be just one post, but it got longer and longer and so I split it into 3 parts. Don’t miss Part 2: Hexo and the IndieWeb (Sending Webmentions) … TerminologyThere are a lot of posts out there which explains the basic concepts of the IndieWeb and Webmentions in particular and you will stumble upon some terms, which has to be explained: Personal Domain … is a domain name that you personally own, control, and use to represent yourself on the internet. Getting a personal domain is the first step towards getting on the indieweb, and is therefore a requirement for IndieMark Level 1 --- indieweb.org (Personal Domain) Microformats … are small patterns of HTML to represent commonly published things like people, events, blog posts, reviews and tags in web pages. They are the quickest & simplest way to provide an API to the information on your website. --- microformats.org (Wiki) POSSE … is an abbreviation for Publish (on your) Own Site, Syndicate Elsewhere, the practice of posting content on your own site first, then publishing copies or sharing links to third parties (like social media silos) with original post links to provide viewers a path to directly interacting with your content. --- indieweb.org (POSSE) Backfeed … is the process of syndicating interactions on your POSSE copies back (AKA reverse syndicating) to your original posts. --- indieweb.org (Backfeed) Web sign-in … is signing in to websites using your personal web address (without having to use your e-mail address). Web sign-in supersedes OpenID. --- indieweb.org (Web sign-in) RelMeAuth … is a proposed open standard for using rel=”me” links to profiles on oauth supporting services to authenticate via either those profiles or your own site. RelMeAuth is the technology behind Web sign-in. --- microformats.org (RelMeAuth) IndieAuth … is a federated login protocol for Web sign-in, enabling users to use their own domain to sign in to other sites and services. --- indieweb.org (IndieAuth) More Info A List Apart: Webmentions: Enabling Better Communication on the Internetindieweb.org: Getting Startedindieweb.org: How to set up web sign-in on your own domainindieweb.org: IndieWeb ExamplesBryce Wray: Webmentions in three SSGs: Part 1Keith J. Grant: Adding Webmention Support to a Static SiteAlessio Caiazza: Articles tagged ´indieweb´* (Forum): Anyone for Webmention?","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Share","slug":"Share","permalink":"https://kiko.io/tags/Share/"}]},{"title":"Triangulate your images with Triangula","subtitle":null,"series":"Great Finds","date":"2021-04-30","updated":"2021-04-30","path":"post/Triangulate-your-images-with-Triangula/","permalink":"https://kiko.io/post/Triangulate-your-images-with-Triangula/","excerpt":"As I am a photo enthusiast I’m always excited to find new tools, to give images a unique look. Today I stumbled over Triangula. Ever seen one of those cool backgrounds, where a picture has been broken up into lots of little triangles? In trigonometry and elementary geometry, the division of a surface into triangles is called a triangular grid, triangular mesh or triangulation.Wikipedia Whoever RH12503 (Ryan H??) is, he did an amazing job on creating this little Go program, including a pleasing UI, do convert images into those equivalents.","keywords":"photo enthusiast im excited find tools give images unique today stumbled triangula cool backgrounds picture broken lots triangles trigonometry elementary geometry division surface called triangular grid mesh triangulationwikipedia rh12503 ryan amazing job creating program including pleasing ui convert equivalents","text":"As I am a photo enthusiast I’m always excited to find new tools, to give images a unique look. Today I stumbled over Triangula. Ever seen one of those cool backgrounds, where a picture has been broken up into lots of little triangles? In trigonometry and elementary geometry, the division of a surface into triangles is called a triangular grid, triangular mesh or triangulation.Wikipedia Whoever RH12503 (Ryan H??) is, he did an amazing job on creating this little Go program, including a pleasing UI, do convert images into those equivalents. These images are absolute great for background images in websites, in order to make the details less recognisable. There is a web version of Triangula, but the desktop version (including a console version) is much faster. Best feature is the ability not only to save the generated images as PNG, but also as SVG! Example with 1.000 Generations var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-yv2kd0\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Triangulated', onHover: true, } }).mount(); Example with 10.000 Generations var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-92hqlu\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Triangulated', onHover: true, } }).mount(); Example with 20.000 Generations var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-6amx7a\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Triangulated', onHover: true, } }).mount();","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"SVG","slug":"SVG","permalink":"https://kiko.io/tags/SVG/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"Forking Hexo plugin 'hexo-index-anything'","subtitle":"Introducing its successor 'hexo-generator-anything'","date":"2021-04-25","updated":"2021-04-25","path":"post/Forking-Hexo-plugin-hexo-index-anything/","permalink":"https://kiko.io/post/Forking-Hexo-plugin-hexo-index-anything/","excerpt":"As I started with this blog 2 years ago, I wanted to document the customization of Hexo to my needs in a series of articles. To group these articles I considered using the build-in categories, but I already used them to group articles by the underlying tech stack or area, like ´JavaScript´, ´C#´ or ´Tools´ and I didn’t want to mix it, as the category was also used in the Url of a post. I was researching another grouping solution for Hexo and stumbled upon hexo-index-anything, a very clever Hexo plugin to generate index pages for almost every FrontMatter variable in a post. As it was freely available under a MIT license on Github, I forked it in July 2020 and made some bug fixes and drop a pull request to Levi … but he unfortunately never answered my pull or issue requests and has set the status of the project to DEPRECATED. Ok then … make a successor on your own, fella…","keywords":"started blog years ago wanted document customization hexo series articles group considered build-in categories underlying tech stack area ´javascript´ ´c#´ ´tools´ didnt mix category url post researching grouping solution stumbled hexo-index-anything clever plugin generate index pages frontmatter variable freely mit license github forked july made bug fixes drop pull request levi … answered issue requests set status project deprecated make successor fella…","text":"As I started with this blog 2 years ago, I wanted to document the customization of Hexo to my needs in a series of articles. To group these articles I considered using the build-in categories, but I already used them to group articles by the underlying tech stack or area, like ´JavaScript´, ´C#´ or ´Tools´ and I didn’t want to mix it, as the category was also used in the Url of a post. I was researching another grouping solution for Hexo and stumbled upon hexo-index-anything, a very clever Hexo plugin to generate index pages for almost every FrontMatter variable in a post. As it was freely available under a MIT license on Github, I forked it in July 2020 and made some bug fixes and drop a pull request to Levi … but he unfortunately never answered my pull or issue requests and has set the status of the project to DEPRECATED. Ok then … make a successor on your own, fella… Basic FunctionalityLet me describe how the original plugin and my successor are working in general: Assume, you have several posts from different authors and you want a list of all posts for every author. The only thing you have to do (after installation and configuration of the plugin), is to add a custom variable called author to the FrontMatter of each post, with the name of the author as value. /source/_posts/my-fancy-post.md1234title: My Fancy Postdate: 2021-04-25 13:41:46author: Kristof... Next time you run hexo generate, several new files will be available in your output folder: One INDEX page … for the author index, with a list of all available authors and the number of the posts: /authors/index.html Many POSTS pages …, one for each author for the author index, with all of the authors posts: /authors/kristof/index.html /authors/ … /index.html New Plugin NameIn order to avoid confusion (and be able to provide the plugin on Github/NPM) I needed a new name for the plugin and due to the fact that it is a generator, I named it simply hexo-generator-anything. New FeaturesThe original code was really hard to read, because every second variable was named ‘index’, therefore I started with some refactorings, before I continued implementing new features. ConfigurationThe original, one-dimensional variable to index mapping, wasn’t meaningful enough for me. The new mapping is now a dictionary and has unique identifiers: /_config.yml123456anything: layout_index: anything-index layout_posts: anything-posts index_mappings: - variable: author path: authors The template* setting names has changed into layout_*, to make more clear what they are meant for. They are pointing to an existing EJS file in your Hexo themes layout folder to render the particular page. layout_indexEJS file (without extension) to render the INDEX page layout_PostsEJS file (without extension) to render each POSTS page On Github and in the NPM package you will find sample EJS files and some partials, to take the SoC pattern into account. The original titleSeparator setting is not longer available, because it is not necessary anymore. By providing a data structure for index with the attributes name and caption, a title can be put together in the EJS file itself. Introducing linked Markdown filesOne of my needs was, to provide more information on the INDEX page and the POSTS pages for each entry or, to stay with the example, for each author. As the _posts folder is used for storing the post data files, I introduced a _anything folder for dropping MD files, which are linked to the index and its values. The subfolders of _anything are representing the index and the files in it each possible value of the index. The file is structured like any other post file: the FrontMatter data at the top and below some content to show along with the the entry to describe it: /source/_anything/authors/kristof.md1234567---title: Kristofgithub: https://github.com/kristofzerbeavatar: kristof.png---... some smart things to say about Kristof, or links or images, whatever ... The INDEX file itself may have its own Markdown file, to provide addition text or data to it, like describing the list. The variable title is a requirement in the Markdown files, but you can add as many variables as you need and use it in the EJS files. All data will be passed through to the template. ConclusionThe new plugin runs very well and it is heavy in use on generating the sections Series and Projects of this blog. Give it a try …","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"}]},{"title":"Discoveries #9","subtitle":null,"series":"Discoveries","date":"2021-04-20","updated":"2021-04-20","path":"post/Discoveries-9/","permalink":"https://kiko.io/post/Discoveries-9/","excerpt":"Todays issue is all about extending your Web Developers toolbox with some useful libraries to provide the best UX to your users or visitors. Tables, Dropdowns, Color Pickers, Footnotes and GDPR dialogs on steroids. JSTableLuckysheetVirtual SelectLC SelectVanilla ColorfulDuet Date PickerCookie ThoughFull-Screen-Touch-SliderBigfootNumber Rollup","keywords":"todays issue extending web developers toolbox libraries provide ux users visitors tables dropdowns color pickers footnotes gdpr dialogs steroids jstableluckysheetvirtual selectlc selectvanilla colorfulduet date pickercookie thoughfull-screen-touch-sliderbigfootnumber rollup","text":"Todays issue is all about extending your Web Developers toolbox with some useful libraries to provide the best UX to your users or visitors. Tables, Dropdowns, Color Pickers, Footnotes and GDPR dialogs on steroids. JSTableLuckysheetVirtual SelectLC SelectVanilla ColorfulDuet Date PickerCookie ThoughFull-Screen-Touch-SliderBigfootNumber Rollup JSTable by Tobias Hägenläuer https://github.com/Trekky12/JSTable JSTable is a library to convert a static HTML TABLE element into an interactive and responsive one, which supports paging, sorting and searching. Luckysheet by MengShu Open Source https://github.com/mengshukeji/Luckysheet Luckysheet is an Excel or Google Sheets clone, for using in your own web projects. It is nearly as powerfull as his role models, but Open Source. Supports imports and has a plugin interface. Virtual Select by Sa Si Dev https://sa-si-dev.github.io/virtual-select/#/ Whoever ‘Sa Si Dev’ is … he/she made a replacement for the ordinary HTML SELECT element, which is nearly unrivalled. It supports search, matched term marking, multi-select, disabling options, option groups, adding new options and many others. One of the best features, is the dialog style select on mobile devices. LC Select by Luca https://lcweb.it/lc-select-javascript-plugin Luca from Italy made this excellent SELECT replacement, which can show the selected options as pills or as grouped lists with images. It supports a search bar, light/dark theme, multilanguage and is mobile ready as well as it has full keyboard support. Vanilla Colorful by Serhii Kulykov https://web-padawan.github.io/vanilla-colorful/ This color picker is just awesome. It looks really pleasing and has all the features you expect. It is written in TypeScript and authored using native ES modules, without dependencies. Duet Date Picker by Duet Design System https://duetds.github.io/date-picker/ This clean looking date picker is easy to integrate and supports mostly everything you can expect: keyboard, mobile devices and even screen readers. Cookie Though by In The Pocket https://cookiethough.dev/ Since the GDPR has hit the industry, there are tons of solutions to show the necessary consent dialog. A team from Belgium has made a really nice one, without misleading buttons, annoying full screen modes or other dark patterns. And it is Open Source… Full-Screen-Touch-Slider by Will Adams https://github.com/bushblade/Full-Screen-Touch-Slider Will has created a really simple, but good looking and animated full screen touch slider, which works great on mobile devices. The only question is: Why is he so addicted to knifes? Bigfoot by Chris Sauve http://www.bigfootjs.com/ Chris from Canada has a smart solution for showing footnotes in HTML documents: show them as bubbles above the text by clicking a tiny button, which replaces the orginial footnote. Only drawback: it’s a jQuery plugin… Number Rollup by marknorrapscm https://marknorrapscm.github.io/number-rollup/ Number Rollup does what the name suggests: it rollups a number from a starting to an end point, animated and customizable. Nice.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Scotch Presets for Lightroom","subtitle":null,"series":"Lightroom Presets","date":"2021-04-18","updated":"2021-04-18","path":"post/Scotch-Presets-for-Lightroom/","permalink":"https://kiko.io/post/Scotch-Presets-for-Lightroom/","excerpt":"Have you ever been in Scotland during summertime? Wonderful tranquility, great scenery and great photo opportunities at every turn. In 2019 I’ve made some great shots there and I want to share the presets with you, that I have developed for processing the photos from my trip through the Highlands and the Isle of Skye.","keywords":"scotland summertime wonderful tranquility great scenery photo opportunities turn ive made shots share presets developed processing photos trip highlands isle skye","text":"Have you ever been in Scotland during summertime? Wonderful tranquility, great scenery and great photo opportunities at every turn. In 2019 I’ve made some great shots there and I want to share the presets with you, that I have developed for processing the photos from my trip through the Highlands and the Isle of Skye. Scotch LightsSo far in the north the light is totally different. When it shines through the clouds, it hits you. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-w6dt2r\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scotch Lights.xmp Scotch StrengthYou must be made of different stuff if you are Scottish. There is hardly anything fine and graceful about the landscape and the weather. It’s rough and tough and you have to deal with it. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-5f452t\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scotch Strength.xmp Scotch SunsetThe view over the North Sea at the edge of the continent is really unique, especially at sunset. Hear the the seagulls scream, the waves crashing against the cliffs and see the stunning colors. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-om8tc1\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scotch Sunset.xmp Scotch EnergyScotland is bold and so full with energy, even in the details. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-dqhw79\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scotch Energy.xmp Scotch TattooThe Edinburgh Military Tattoo, which takes place twice a year, is a feast for the senses. The incredible sound on one hand and the colors on the other. The brass, the feathers, the uniforms stand out in the setting sun. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-efjvrx\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Scotch Tattoo.xmp","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Presets","slug":"Presets","permalink":"https://kiko.io/tags/Presets/"}]},{"title":"Adding Screenshots to Trello Cards on Android","subtitle":null,"series":"Step By Step","date":"2021-04-11","updated":"2021-04-11","path":"post/Adding-Screenshots-to-Trello-Cards-on-Android/","permalink":"https://kiko.io/post/Adding-Screenshots-to-Trello-Cards-on-Android/","excerpt":"I’m collecting interesting One-Page-Tools on the web on a Trello board. To add a new card, I use a simple little script on my Android smartphone, I wrote about here: Add website to Trello card the better way. On processing the page to store on a card, Trello scrapes the page and takes the <meta> tag og:image out of the HTML to generate an image attachment and take it as cover for the card. This sometimes works, but most of the time it doesn’t, because website owners often don’t pay attention to reasonable <meta> tags. Because it is easier to find a card with visual support, I create my own screenshots for the cards in a manual, but streamlined, process, I want to show you here.","keywords":"im collecting interesting one-page-tools web trello board add card simple script android smartphone wrote website processing page store scrapes takes <meta> tag ogimage html generate image attachment cover works time doesnt owners dont pay attention reasonable tags easier find visual support create screenshots cards manual streamlined process show","text":"I’m collecting interesting One-Page-Tools on the web on a Trello board. To add a new card, I use a simple little script on my Android smartphone, I wrote about here: Add website to Trello card the better way. On processing the page to store on a card, Trello scrapes the page and takes the <meta> tag og:image out of the HTML to generate an image attachment and take it as cover for the card. This sometimes works, but most of the time it doesn’t, because website owners often don’t pay attention to reasonable <meta> tags. Because it is easier to find a card with visual support, I create my own screenshots for the cards in a manual, but streamlined, process, I want to show you here. PrerequisitesOS: Android 5 and above Apps: Trello https://play.google.com/store/apps/details?id=com.trello Screenshot Touch https://play.google.com/store/apps/details?id=com.mdiwebma.screenshot Step 1 Open up Screenshot Touch and set under Shaking and Delay the option Caption by shaking [1]. Set the Shake sensitivity option to Hard [2]. Set the Resize option to 50% [3]. Start the capture monitoring service [4]. Step 2 Open up Trello and got to the card, where you want to add an screenshot for use as an cover. Click on the URL attachment to open it in your browser [1]. Step 3 Shake you smartphone to capture the current website. Step 4 Switch to Screenshot Touch and open the Photo Viewer [1] Step 5 Open the Crop Image dialog. Step 6 Set the Crop Mode to 1:1 to get a squared image. It will be persisted in the apps settings for further use. Step 7 Place the overlay to select you preferred part of the image [1]. Save the image [2]. Step 8 Switch back to Trello and click to add a new attachment [1]. Step 9 Choose to add from file [1]. Step 10 In Androids file dialog, head up to Recent Files [1]. Select the new image [2]. Step 11 In the context menu of the added attachment, select Make Card Cover [1], if Trello has not done it yet. ResultOnce you have done this two or three times, the process takes less than 30 seconds.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Trello","slug":"Trello","permalink":"https://kiko.io/tags/Trello/"},{"name":"Android","slug":"Android","permalink":"https://kiko.io/tags/Android/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"SVG Resources","subtitle":null,"series":"Great Finds","date":"2021-04-09","updated":"2021-04-09","path":"post/SVG-Resources/","permalink":"https://kiko.io/post/SVG-Resources/","excerpt":"#post-SVG-Resources button { background-color: #f1f1f1; border: none; padding: 1rem; margin-bottom: 1rem; margin-right: 1rem; cursor: pointer; } #post-SVG-Resources em { opacity: 0.33; } #post-SVG-Resources #info { color: silver; display: block; height: 24px; } #post-SVG-Resources #info.result { color: green; font-weight: bold; } var timeoutID; function setInfo(e,m) { let info = document.getElementById(e); info.textContent = m; info.classList.add(\"result\"); window.clearTimeout(timeoutID); timeoutID = setTimeout(function() { info.textContent = \"Guess and click...\"; info.classList.remove(\"result\"); window.clearTimeout(timeoutID); }, 2000); } Since beginning beginning of time, people are using symbols to make things clear quickly and easily. So do we when developing websites and web apps by using icons. Everybody knows what’s behind a loupe symbol or a hamburger icon. Guess and click... The way we implement icons have changed in the past. From BMP files to GIF and JPG files, PNG files, to complete or customizable symbol fonts like fontello.com, to Scalable Vector Graphics (SVG). SVG’s in particular are becoming increasingly popular, because they are nothing more than XML-like code, that can be manipulated via CSS or JS, their digital footprint is unbeatable small and they scale seemlessly. Dealing with SVG’s is a little bit more difficult than placing a PNG in HTML, because of its complexity, but it is worth learning as much as possible about it. So did I in the last couple of month and I want to share my finds on the web with you in this post.","keywords":"#post-svg-resources button { background-color #f1f1f1 border padding 1rem margin-bottom margin-right cursor pointer } em opacity #info color silver display block height 24px #inforesult green font-weight bold var timeoutid function setinfoem info = documentgetelementbyide infotextcontent infoclasslistaddresult windowcleartimeouttimeoutid settimeoutfunction guess click infoclasslistremoveresult beginning time people symbols make things clear quickly easily developing websites web apps icons whats loupe symbol hamburger icon implement changed past bmp files gif jpg png complete customizable fonts fontellocom scalable vector graphics svg svgs increasingly popular xml-like code manipulated css js digital footprint unbeatable small scale seemlessly dealing bit difficult placing html complexity worth learning couple month share finds post","text":"#post-SVG-Resources button { background-color: #f1f1f1; border: none; padding: 1rem; margin-bottom: 1rem; margin-right: 1rem; cursor: pointer; } #post-SVG-Resources em { opacity: 0.33; } #post-SVG-Resources #info { color: silver; display: block; height: 24px; } #post-SVG-Resources #info.result { color: green; font-weight: bold; } var timeoutID; function setInfo(e,m) { let info = document.getElementById(e); info.textContent = m; info.classList.add(\"result\"); window.clearTimeout(timeoutID); timeoutID = setTimeout(function() { info.textContent = \"Guess and click...\"; info.classList.remove(\"result\"); window.clearTimeout(timeoutID); }, 2000); } Since beginning beginning of time, people are using symbols to make things clear quickly and easily. So do we when developing websites and web apps by using icons. Everybody knows what’s behind a loupe symbol or a hamburger icon. Guess and click... The way we implement icons have changed in the past. From BMP files to GIF and JPG files, PNG files, to complete or customizable symbol fonts like fontello.com, to Scalable Vector Graphics (SVG). SVG’s in particular are becoming increasingly popular, because they are nothing more than XML-like code, that can be manipulated via CSS or JS, their digital footprint is unbeatable small and they scale seemlessly. Dealing with SVG’s is a little bit more difficult than placing a PNG in HTML, because of its complexity, but it is worth learning as much as possible about it. So did I in the last couple of month and I want to share my finds on the web with you in this post. Using SVG’s in briefThe most useful way of using SVG’s is as an image out of a file, either directly …: (use DevTools [F12] to inspect the element) 1<img src="images/options.svg" /> … or as a background image: button.options { height: 56px; width: 56px; background-image: url(options.svg); background-repeat: no-repeat; background-position: 50% 50%; } (use DevTools [F12] to inspect the element) 1234567891011<style> button.options { height: 56px; width: 56px; background: url(images/options.svg); background-repeat: no-repeat; background-position: 50% 50%; }</style><button class="options"></button> As files, no matter how small, has to be requested from the server, you can also define SVG’s inline for better performance: (use DevTools [F12] to inspect the element) 12345678910111213<body> ... <button> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"> <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M7 3C8.86384 3 10.4299 4.27477 10.874 6H19V8H10.874C10.4299 9.72523 8.86384 11 7 11C4.79086 11 3 9.20914 3 7C3 4.79086 4.79086 3 7 3ZM7 9C8.10457 9 9 8.10457 9 7C9 5.89543 8.10457 5 7 5C5.89543 5 5 5.89543 5 7C5 8.10457 5.89543 9 7 9Z" /> <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M17 20C15.1362 20 13.5701 18.7252 13.126 17H5V15H13.126C13.5701 13.2748 15.1362 12 17 12C19.2091 12 21 13.7909 21 16C21 18.2091 19.2091 20 17 20ZM17 18C18.1046 18 19 17.1046 19 16C19 14.8954 18.1046 14 17 14C15.8954 14 15 14.8954 15 16C15 17.1046 15.8954 18 17 18Z" /> </svg> </button> ...</body> If you want to use a SVG multiple times, you can define it once by wrapping it up in a symbol tag with an id and use it wherever you want: (use DevTools [F12] to inspect the elements) 12345678910111213141516171819<body> <svg xmlns="http://www.w3.org/2000/svg"> <symbol id="options" width="24" height="24" viewBox="0 0 24 24" fill="none"> <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M7 3C8.86384 3 10.4299 4.27477 10.874 6H19V8H10.874C10.4299 9.72523 8.86384 11 7 11C4.79086 11 3 9.20914 3 7C3 4.79086 4.79086 3 7 3ZM7 9C8.10457 9 9 8.10457 9 7C9 5.89543 8.10457 5 7 5C5.89543 5 5 5.89543 5 7C5 8.10457 5.89543 9 7 9Z" /> <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M17 20C15.1362 20 13.5701 18.7252 13.126 17H5V15H13.126C13.5701 13.2748 15.1362 12 17 12C19.2091 12 21 13.7909 21 16C21 18.2091 19.2091 20 17 20ZM17 18C18.1046 18 19 17.1046 19 16C19 14.8954 18.1046 14 17 14C15.8954 14 15 14.8954 15 16C15 17.1046 15.8954 18 17 18Z" /> </symbol> </svg> ... <button> <svg width="24" height="24"><use xlink:href="#options" /></svg> </button> ... <button> <svg width="24" height="24"><use xlink:href="#options" /></svg> </button> ...</body> SVG ResourcesFinding the right SVG for your project is time consuming, like it is for symbol fonts or PNG’s. So here are a few tips getting SVG’s for free: css.gg https://css.gg 700+ icons, downloadable as SVG, PNG, XD, Figma, Styled Component (Typescript) or even pure CSS. Tabler Icons https://tabler-icons.io Over 1.250 icons in several categories, downloadable as SVG or PNG. Boxicons https://boxicons.com 1.500 regular or filled icons, downloadable as SVG or PNG. Supports animations, Web Components and is also available as font. Feather https://feathericons.com 268 icons as SVG, with customizable size, stroke with and color. Majesticons https://majesticons.com 210 line and solid icons, with Figma support and also available as Github repository. Simple icons https://simpleicons.org Over 1800 icons of popular brands, with hex color code. SVG Repo https://www.svgrepo.com 300.000+ vectors and icons in over 400 collections from different artists. Google Fonts - Material Icons https://fonts.google.com/icons At least … an own frontend of Googles Material Icons inside Google Fonts for downloading them individually as SVG, PNG or Android/iOS package. Last but not least, SVG is more powerful then drawing stuff. It’s possible to add raster images, text with a particular font and use many CSS-like techniques like gradients and animations. See links below… More Info CSS Tricks: Using SVGCSS-Tricks: Use and Reuse Everything in SVG… Even Animations!CSS Tricks: An SVG That Isn’t All… SVGmediaevent.de: Sieben Wege, SVG in HTML-Seiten zu setzen (German)Foxland: Simple and Accessible SVG Menu Hamburger Animation","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"SVG","slug":"SVG","permalink":"https://kiko.io/tags/SVG/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"}]},{"title":"Discoveries #8","subtitle":null,"series":"Discoveries","date":"2021-03-31","updated":"2021-03-31","path":"post/Discoveries-8/","permalink":"https://kiko.io/post/Discoveries-8/","excerpt":"This month my discoveries are all about CSS … at least almost. See the stunning solutions developers around the world have created and take them to improve yours. Have fun exploring. Charts.cssAnimXYZMagic Animationstransition.cssMake Animated Content Placeholders with HTML and CSSAnimating UnderlinesNew aspect-ratio CSS propertyHow to display language-specific quotes in CSSMaking the DETAILS element look and behave like a modalBetter Line Breaks for Long URLs","keywords":"month discoveries css … stunning solutions developers world created improve fun exploring chartscssanimxyzmagic animationstransitioncssmake animated content placeholders html cssanimating underlinesnew aspect-ratio propertyhow display language-specific quotes cssmaking details element behave modalbetter line breaks long urls","text":"This month my discoveries are all about CSS … at least almost. See the stunning solutions developers around the world have created and take them to improve yours. Have fun exploring. Charts.cssAnimXYZMagic Animationstransition.cssMake Animated Content Placeholders with HTML and CSSAnimating UnderlinesNew aspect-ratio CSS propertyHow to display language-specific quotes in CSSMaking the DETAILS element look and behave like a modalBetter Line Breaks for Long URLs Charts.css by Rami Yushuvaev and Lana Gordiievski https://chartscss.org Mentioned in hundreds of other blog posts earlier, Chart.css is so good that I have to mention it here too. Pure CSS charts, with animations, responsiveness, customizable and Open Source … what more could a heart desire? AnimXYZ by Miles and Mattan Ingram https://animxyz.com AnimXYZ is a CSS library for composing animations, powered by CSS variables. It has Vue and React support. Magic Animations by Christian Pucci https://www.minimamente.com/project/magic Christian from Italy brings us an animation library with 64 beautiful effects, to get started directly. transition.css by Adam Argyle https://github.com/argyleink/transition.css Another CSS library for animating things on the web. Adam is targeting the transition of an element. Cool and easy to use. Make Animated Content Placeholders with HTML and CSS by James Sinkala https://dev.to/xinnks/make-animated-content-placeholders-with-html-and-css-3ekn A modern approach to entertain web users while loading some content or images is to show animated placeholders, like Instagram, Facebook and others do. James gives us the instructions how to implement these with pre CSS. Animating Underlines by Michelle Barker https://css-irl.info/animating-underlines From the beginning of time, URL’s has shown as underlined text. How boring. Michelle has some ideas to bring some life into links. New aspect-ratio CSS property by Una Kravets https://web.dev/aspect-ratio Dealing with images in CSS can be a mess sometimes, especially on responsive layouts. Read about the common hacks regarding aspect ratio and the upcoming new CSS feature aspect-ratio. How to display language-specific quotes in CSS by Stefan Judis https://www.stefanjudis.com/today-i-learned/how-to-use-language-dependent-quotes-in-css Doing internationalization right, you have to beware of some pitfalls, like the different quotes in some languages. Germans are using different double quotes for start and end, French are using double arrows and so on. Stefan shows us how to do it right. Making the DETAILS element look and behave like a modal by Niels Voogt https://codepen.io/NielsVoogt/full/XWjPdjO In this pen, Niels is playing around with the DETAILS tag and shows how it can be used for a modal dialog with CSS only. Great idea! Better Line Breaks for Long URLs by Reuben Lillie https://css-tricks.com/better-line-breaks-for-long-urls Reuben addresses in his post at CSS-Tricks the problem of displaying long URL’s and shows a solution with a little bit JavaScript how to do it right once and for all.","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Custom Caller Authentication with ASP.NET Core 5.0 Web API","subtitle":null,"date":"2021-02-28","updated":"2021-02-28","path":"post/Custom-Caller-Authentication-with-ASP-NET-Core-5-0-WebApi/","permalink":"https://kiko.io/post/Custom-Caller-Authentication-with-ASP-NET-Core-5-0-WebApi/","excerpt":"Developing micro services with Microsoft ASP.NET Core 5.0 Web API is powerful and fun, but the fun stops, if your data are accesses unauthorized. It is absolutely fundamental to have a protection layer, which filters out unwanted data requests. A common way is to limit the service access by providing API Keys to well known clients. In this post I will show you how to implement such a filter in terms of API keys and IP addresses.","keywords":"developing micro services microsoft aspnet core web api powerful fun stops data accesses unauthorized absolutely fundamental protection layer filters unwanted requests common limit service access providing keys clients post show implement filter terms ip addresses","text":"Developing micro services with Microsoft ASP.NET Core 5.0 Web API is powerful and fun, but the fun stops, if your data are accesses unauthorized. It is absolutely fundamental to have a protection layer, which filters out unwanted data requests. A common way is to limit the service access by providing API Keys to well known clients. In this post I will show you how to implement such a filter in terms of API keys and IP addresses. The SettingsLets start with the list of clients, who should be able to access the data. The most useful place for this is in the appsettings.json of the Core 5.0 Web API project: appsettings.json123456789101112131415..."Callers": [ { "Name": "localhost", "ApiKey": null, "IPAddress": "::1" }, { "Name": "John Doe", "ApiKey": "mytopsecretapikeyforjohndoe", "IPAddress": "*" } ]... This list has two entries: one for the server itself (“localhost”), which is restricted to the local IP address "::1", and one for the test user "John Doe", who can access from any IP address ("*"), but must supply his personal API key with his requests. In order to handle this setting, we have to introduce it to the system at startup as a class: CallerSetting.cs123456public class CallerSetting{ public string Name { get; set; } public string ApiKey { get; set; } public string IPAddress { get; set; }} Startup.cs1234567891011...public void ConfigureServices(IServiceCollection services) { ... IConfigurationSection configSection = Configuration.GetSection("Callers"); services.Configure<List<CallerSetting>>(configSection);}... The ControllerLet’s assume we have a controller, which handles the API requests, like this: MyFancyController.cs123456789101112131415using Microsoft.AspNetCore.Mvc;namespace MyAPIProject{ [Route("api/helloworld")] [ApiController] public class MyFancyAPIController : ControllerBase { [HttpGet] public string Get() { return "Hello World"; } }} To prevent to write a request check against our new settings in each action method, we can decorate the whole controller class by introducing an new custom Attribute, which will do the work: MyFancyController.cs1234567...[Route("api/helloworld")][ApiController][AuthenticateApiRequest]public class MyFancyAPIController : ControllerBase... The AttributeHere is the code for the new attribute. It uses the IActionFilter. These filters run within the ASP.NET Core action invocation pipeline, in our case BEFORE the action is entered (OnActionExecutionAsync). AuthenticateApiRequestAttribute.cs1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859using Microsoft.AspNetCore.Mvc;using Microsoft.AspNetCore.Mvc.Filters;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;using System;using System.Collections.Generic;using System.Threading.Tasks;using System.Linq;namespace MyAPIProject{ [AttributeUsage(AttributeTargets.Class)] public class AuthenticateApiRequestAttribute : Attribute, IAsyncActionFilter { public async Task OnActionExecutionAsync ( ActionExecutingContext context, ActionExecutionDelegate next ) { // Get an Api Key from Request Header context.HttpContext.Request.Headers.TryGetValue( "ApiKey", out var requestApiKey ); // Get the remote IP Address var requestIpAddress = context.HttpContext.Connection.RemoteIpAddress.ToString(); // Get access to 'appsettings.json' var appSettings = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>(); // Get 'Callers' list from settings var callers = appSettings.GetSection("Callers") .Get<List<CallerSetting>>(); // Get all Callers with matching IP Adress and/or API Key via LINQ var current = callers.Where(c => (c.IPAddress == requestIpAddress) || (c.IPAddress == "*" && c.ApiKey == requestApiKey) || (c.IPAddress == requestIpAddress && c.ApiKey == requestApiKey)); // Do we have a match? if (current.Count() == 0) { // No, then return with an error context.Result = new ContentResult() { StatusCode = 401, Content = "Unauthorized Access" }; return; } await next(); } }} The Result More Info Microsoft Docs: Filters in ASP.NET CoreMicrosoft Docs: Configuration in ASP.NET Core","categories":[{"name":".NET","slug":"NET","permalink":"https://kiko.io/categories/NET/"}],"tags":[{"name":"Visual Studio","slug":"Visual-Studio","permalink":"https://kiko.io/tags/Visual-Studio/"},{"name":"WebAPI","slug":"WebAPI","permalink":"https://kiko.io/tags/WebAPI/"},{"name":"Authentication","slug":"Authentication","permalink":"https://kiko.io/tags/Authentication/"}]},{"title":"Discoveries #7","subtitle":null,"series":"Discoveries","date":"2021-02-25","updated":"2021-02-25","path":"post/Discoveries-7/","permalink":"https://kiko.io/post/Discoveries-7/","excerpt":"February and the first sunny days in 2021. What a delight! Have fun, sitting in the sun, discovering my newest finds on the web. This time, all regarding JavaScript… github1s: One second to read GitHub code with VS CodeHow to enhance fetch() with the Decorator PatternKy - Delightful HTTP RequestsVS Code’s REST Client Plugin is All You Need to Make API Callsjson-viewYou might not need jQueryJavaScript Algorithms and Data Structuresdate-fns - Modern JavaScript date utility libraryParsing Markdown into an Automated Table of ContentsFakeScroll - lightweight custom-looking scrollbars","keywords":"february sunny days delight fun sitting sun discovering newest finds web time javascript… github1s read github code codehow enhance fetch decorator patternky - delightful http requestsvs codes rest client plugin make api callsjson-viewyou jqueryjavascript algorithms data structuresdate-fns modern javascript date utility libraryparsing markdown automated table contentsfakescroll lightweight custom-looking scrollbars","text":"February and the first sunny days in 2021. What a delight! Have fun, sitting in the sun, discovering my newest finds on the web. This time, all regarding JavaScript… github1s: One second to read GitHub code with VS CodeHow to enhance fetch() with the Decorator PatternKy - Delightful HTTP RequestsVS Code’s REST Client Plugin is All You Need to Make API Callsjson-viewYou might not need jQueryJavaScript Algorithms and Data Structuresdate-fns - Modern JavaScript date utility libraryParsing Markdown into an Automated Table of ContentsFakeScroll - lightweight custom-looking scrollbars github1s: One second to read GitHub code with VS Code by netcon (conwnet) https://github.com/conwnet/github1s How do you peak in the code of a Github repository? Navigate back and forth on github.com? The chinese developer netcon from Shenzhen has better idea: just add the 2 characters 1s to the github url and the repository opens up in the new version of VSCode, which now can be built for browsers. Pretty handy… How to enhance fetch() with the Decorator Pattern by Dmitri Pavlutin https://dmitripavlutin.com/enhance-fetch-with-decorator-pattern/ Fetching JSON files with JavaScript means to call fetch() asynchronously and pick the response manually. Two AWAITS and a lot of stuff can go wrong. Dmitri shows how to construct a class which enables you to do this in one step. Ky - Delightful HTTP Requests by Sindre Sorhus https://github.com/sindresorhus/ky Fetch is nice, but if you want it nice and easy, you have to rely on a 3rd-party library, like *Ky. Sindre Sorhus did a great job to bring fetching in one line, within around 13KB. VS Code’s REST Client Plugin is All You Need to Make API Calls by Paige Niedringhaus https://blog.bitsrc.io/vs-codes-rest-client-plugin-is-all-you-need-to-make-api-calls-e9e95fcfd85a Using Postman or Nightingale for testing your microservices? Not absolutely necessary, as there are possibilities to do it right in VSCode, as Paige show us in her post here. No need to leave your editor. json-view by Pavel https://github.com/pgrabovets/json-view It’s not often that a developer has to display raw JSON data on a website or app. Pavel from the Ukraine has a solution to do this with style. You might not need jQuery by Zack Bloom and Adam Schwartz http://youmightnotneedjquery.com/ Many of us relied on jQuery in the past. So did Zack Bloom and Adam Schwartz as I suppose. They have published a website, that contrasts the native JavaScript methods for the most common jQuery methods. Go Vanilla, go! JavaScript Algorithms and Data Structures by Oleksii Trekhleb https://github.com/trekhleb/javascript-algorithms Oleksii has collected a huge bunch of useful JS methods in his Github repository and has translated the docs for every method into 14 (!) languages. Whoop … what a job! Ever wanted to know how to calculate the Euclidean Distance? Oleksii has the answer and the code. date-fns - Modern JavaScript date utility library by {Many} https://date-fns.org/ moment.js, maybe the most used JS library for calculating dates, is now in maintenance mode, because it is getting on in years. A good alternative is date-fns, which supports tree-shaking and other modern approaches. In addition to that, you will find here and here good comparisons between several date libraries or even native JS. Parsing Markdown into an Automated Table of Contents by Lisi Linhart https://css-tricks.com/parsing-markdown-into-an-automated-table-of-contents/ A well-structured text has headings, subheadings and paragraphs. For the web we often write our stuff in Markdown. Lisi shows us how to process such a Markdown file to get a TOC automatically. FakeScroll - lightweight custom-looking scrollbars by Yair Even Or https://github.com/yairEO/fakescroll The scrollbar belongs to the website or app a developer is creating, in my opinion. Therefore it is a mess what browser manufacturers offer developers in terms of possibilities. Yair has constructed a JS library which replaces the build-in scrollbars completely with standard HTML elements. Nice…","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Native JavaScript Multilanguage Templating","subtitle":null,"date":"2021-02-24","updated":"2021-02-24","path":"post/Native-JavaScript-Multilanguage-Templating/","permalink":"https://kiko.io/post/Native-JavaScript-Multilanguage-Templating/","excerpt":"In the project I’m currently working on, I faced the “problem” to integrate multilanguage support, but due to the fact that the new app should be written in vanilla JS, without any plugins, libraries or other dependencies, I had to develop my own localization layer. In this article I want to show you my approach on this…","keywords":"project im working faced problem integrate multilanguage support due fact app written vanilla js plugins libraries dependencies develop localization layer article show approach this…","text":"In the project I’m currently working on, I faced the “problem” to integrate multilanguage support, but due to the fact that the new app should be written in vanilla JS, without any plugins, libraries or other dependencies, I had to develop my own localization layer. In this article I want to show you my approach on this… My solution is based on a template system that I implemented into my project at an earlier stage. If you are interested in how this works, I recommend you read my article Utilize a repository of reusable ES6 template literals. Let’s start with the standard scaffold of an HTML5 app, extended with some style‘s, an initialization script and a lonely main element, we want to fill with some localized content: index.html1234567891011121314151617181920212223242526<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8" /> <meta http-equiv="language" content="en"> <title>Native JavaScript Multilanguage Templating</title> <style> body { padding: 2rem; } main { text-align: center; } </style> <script type="module"> import { App } from './app.js'; window.app = new App(); app.init(); </script> </head> <body> <main></main> </body></html> The script points to the following ES6 module class in the file app.js: app.js1234567891011class App { constructor() { // do something when the class is instantiated } init() { // do something to initialize the app }}export { App }; Nothing uncommon so far, if you are familiar with ES6 classes and imports/exports. Now let’s create a localizations.js file, to store the needed localized strings in all wanted languages. Every language will have its own branch in a Localizations object, represented by its two-letter ISO-639-1 language code. All translations are accessible via an unique english key word: localizations.js12345678910111213141516171819202122export function Localizations() { return { "EN": { "helloWorld": "Hello World" }, "DE": { "helloWorld": "Hallo Welt" }, "ES": { "helloWorld": "Hola, mundo" }, "FR": { "helloWorld": "Bonjour le monde" }, "RU": { "helloWorld": "Здравствуйте, мир" }, "JP": { "helloWorld": "ハローワールド" } }} As we import the localizations.js in our app.js, we can initialize the localizations in the constructor of the app class with the language code of the users browser: app.js12345678910111213141516import { Localizations } from './localizations.js';class App { constructor() { // Get browser language this.langCode = window.navigator.language.split("-")[0].toUpperCase(); // Init localization to access via 'app.localization' globally this.localization = Localizations()[this.langCode]; } ... app.localization now holds the key/value list of the current language. Now we implement the templating class, as described in Utilize a repository of reusable ES6 template literals and define a first template called helloWorld … templates.js123456789101112131415class Templates { helloWorld(data) { return this.fillTemplate(` <h1>${app.localization.helloWorld}</h1> `, data); } fillTemplate(templateString, templateVars){ var func = new Function(...Object.keys(templateVars), "return `" + templateString + "`;"); return func(...Object.values(templateVars)); }}export { Templates }; The inner text of the h1 element in the helloWorld template refers to the globally available variable app.localization, we initialized in the last step, and points to the translation helloWorld. In app.js we import the templates.js and implement some code in the init method, to get the template and bring it to the DOM: app.js1234567891011121314151617import { Localizations } from './localizations.js';import { Templates } from './templates.js';class App { ... init() { // Get "Hello World" H1 element in current language let helloWorld = app.templates.helloWorld({}); //Insert H1 element into MAIN element document.querySelector("main") .insertAdjacentHTML("beforeend", helloWorld); } This is it … In the following Github repository you will find a solution based on this example, extented with a language selector, cookie support and some helper methods to keep the code nice and clean: https://github.com/kristofzerbe/Native-JavaScript-Multilanguage-Templating","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Localization","slug":"Localization","permalink":"https://kiko.io/tags/Localization/"},{"name":"ES6","slug":"ES6","permalink":"https://kiko.io/tags/ES6/"},{"name":"Templating","slug":"Templating","permalink":"https://kiko.io/tags/Templating/"}]},{"title":"Remote Testing and Debugging with Chrome","subtitle":"How to test a local site on a mobile device and debug it locally","series":"Step By Step","date":"2021-01-24","updated":"2021-01-24","path":"post/Remote-Testing-and-Debugging-with-Chrome/","permalink":"https://kiko.io/post/Remote-Testing-and-Debugging-with-Chrome/","excerpt":"Developing a website or web app means, you have installed an editor locally on your computer, writing your code locally and start a tiny, built-in web server for debugging locally in your preferred browser. In most browsers, there are some features to mimic a smartphone, to see if your solution is working on such a device too, but you only get a hint if it’s running properly. Some mobile features like navigator.canShare do not work at all. Better is to see it live on your device. This article will show you firstly, how to test your local solution on a smartphone and secondly, how to debug it locally, when it runs on the smartphone after releasing.","keywords":"developing website web app means installed editor locally computer writing code start tiny built-in server debugging preferred browser browsers features mimic smartphone solution working device hint running properly mobile navigatorcanshare work live article show firstly test local debug runs releasing","text":"Developing a website or web app means, you have installed an editor locally on your computer, writing your code locally and start a tiny, built-in web server for debugging locally in your preferred browser. In most browsers, there are some features to mimic a smartphone, to see if your solution is working on such a device too, but you only get a hint if it’s running properly. Some mobile features like navigator.canShare do not work at all. Better is to see it live on your device. This article will show you firstly, how to test your local solution on a smartphone and secondly, how to debug it locally, when it runs on the smartphone after releasing. I will use following setup: Editor: Visual Studio Code Smartphone: Android Browser for Desktop & Mobile: Microsoft Edge (any other Chromium based browser will work also) Before we start, we have to enable the Android smartphone to connect to other devices, by switching on USB Debugging: Enable the Developer Options Go to Settings > About Phone Tap 7 times on Build Number Enable USB Debugging Go to Settings > System > Advanced > Developer Options Switch USB debugging to ON 1. Test your local site on a mobile deviceWhen you start your local web server from VS Code, your solution can be accessed by a localhost address at a specific port: Even if you are in the same network with all your devices, this address is only available locally. You need to “announce” this address to your mobile device by using the mechanism called Port Forwarding. This is a job for the browser… Connect you mobile device via USB with your local machine Open up chrome://inspect/#devices in your Chromium based browser (works in all Chromium browsers) Your mobile device will ask you to allow USB-Debugging … say ALLOW Under Devices, your mobile device will appear after a few seconds … my is here the Pixel 4 Click on Port Forwarding Enter your local, to be forwarded address ('localhost:' and port number) and check Enable port forwarding Open your Chromium based browser on your mobile device Enter the URL localhost:4000 Your local solution will now be loaded on your mobile device and you will see this in your local DevTools: 2. Debug a site running on your mobile device locallyThis step is now very easy, because we are connected to the mobile device and a remote site is loaded. Just click inspect at the appropriate item: . This works now also on the released version of your solution, you want to debug. Just enter the URL in a new tab on your mobile device, find the item in DevTools-Devices and click on inspect. The window, which will be opened on inspect, are the Chrome Developer Tools and every interaction with it, will be reflected on your mobile device, as you are used to when debugging locally: More Info Chrome DevTools: Access Local ServersChrome DevTools: Get Started with Remote Debugging Android Devices","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"},{"name":"Debugging","slug":"Debugging","permalink":"https://kiko.io/tags/Debugging/"}]},{"title":"Discoveries #6","subtitle":null,"series":"Discoveries","date":"2021-01-20","updated":"2021-01-20","path":"post/Discoveries-6/","permalink":"https://kiko.io/post/Discoveries-6/","excerpt":"2020 is over and history. Well, may 2021 be with us. With this post I would like to continue the Discoveries, with new momentum. There was a lot to read over the holidays. All the finds in this issue have something to do with visual aspects of web design in the broadest sense. Drop-Shadow: The Underrated CSS Filtercss.gg - 700+ CSS IconsSVGBox - API for Web IconsChange Color of SVG on HoverCSS filter generator to convert from black to target hex colorResponsive && Configurable SVG WavesBalloon.cssHow to Build a CSS-only Organizational ChartShiftyBrad Traversy's 50 Projects 50 Days","keywords":"history post continue discoveries momentum lot read holidays finds issue visual aspects web design broadest sense drop-shadow underrated css filtercssgg - 700+ iconssvgbox api iconschange color svg hovercss filter generator convert black target hex colorresponsive && configurable wavesballooncsshow build css-only organizational chartshiftybrad traversy's projects days","text":"2020 is over and history. Well, may 2021 be with us. With this post I would like to continue the Discoveries, with new momentum. There was a lot to read over the holidays. All the finds in this issue have something to do with visual aspects of web design in the broadest sense. Drop-Shadow: The Underrated CSS Filtercss.gg - 700+ CSS IconsSVGBox - API for Web IconsChange Color of SVG on HoverCSS filter generator to convert from black to target hex colorResponsive && Configurable SVG WavesBalloon.cssHow to Build a CSS-only Organizational ChartShiftyBrad Traversy's 50 Projects 50 Days Drop-Shadow: The Underrated CSS Filter by Michelle Barker https://css-irl.info/drop-shadow-the-underrated-css-filter There are 2 built-in ways to drop a shadow on a HTML element with CSS. Michelle shows us the difference and the additional features filter: drop-shadow has. css.gg - 700+ CSS Icons by Astrit Malësia https://css.gg Astrit, a swedish designer, has build this outstanding icon repository, with tons of icons in pure CSS or SVG. Chapeau… SVGBox - API for Web Icons by ? https://svgbox.net In case css.gg has not the appropriate SVG icon you are looking for, visit this site and explorer 12 icon sets with over 3.000 icons. Whoever brought this to us, thanks. Change Color of SVG on Hover by Chris Coyier https://css-tricks.com/change-color-of-svg-on-hover Once again Chris, who shows us here, how to colorize a SVG icon in case you use it as a background image by using CSS filter. CSS filter generator to convert from black to target hex color by Barrett Sonntag https://codepen.io/sosuke/pen/Pjoqqp As Chris Coyer mentioned in the discovery above, you need the appropriate filter values on colorizing SVG’s. Barret has developed a converter in a pen. Useful tool… Responsive && Configurable SVG Waves by Jhey Tompkins https://codepen.io/jh3y/pen/poEvKxo Seperating content on a web site with some kind of divider is advisable and motion is nice, if you don’t overdo it. Jhey shows us, how to combine both with an animated wave. Balloon.css by Claudio Holanda https://kazzkiq.github.io/balloon.css Ever needed tooltips for elements, which are not self-describing? Download this pure CSS solution from Claudio und you never search again for something like that. How to Build a CSS-only Organizational Chart by someone at Envato Tuts+ https://codepen.io/tutsplus/pen/MWedpoj Org charts are important to visualize hierarchies. Why not creating them with nothing else than HTML and CSS? Shifty by Warren Galyen https://wgalyen.github.io/shifty No one can escape parallax effects on backgrounds. They are just too pleasing. Warren addresses this with his tiny JavaScript library. Brad Traversy's 50 Projects 50 Days by Brad Traversy https://github.com/bradtraversy/50projects50days There are a lot of doing-a-thing-every-day projects, but Brad is pushing it with his 50 web projects in 50 days. Check out Expanding Cards or Rotating Navigation Animation or Theme Clock. Cool stuff and source code is available.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Use a duplicate image to drop a shadow","subtitle":"An alternative for 'box-shadow' on images","date":"2021-01-20","updated":"2021-01-20","path":"post/Use-a-duplicate-image-to-drop-a-shadow/","permalink":"https://kiko.io/post/Use-a-duplicate-image-to-drop-a-shadow/","excerpt":"Depending on your design, sometimes it is nice to drop a shadow on an image to highlight it: 1<img src="my-image.jpg" /> 123img { box-shadow: 0px 25px 25px -10px #666;} But … it looks like a paper print of the image, with a light bulb in the first third above it. The shade is grey, boring and has been used and seen many times before… An design related Instagram post from Muhammad Abdull of thewilsonthings, inspired me to use the image itself as the shadow in order to make the image look a bit translucent. Should be the same technique as that of a reflection. Here is the HTML/CSS code for it, as the people asking for it in the comments.","keywords":"depending design nice drop shadow image highlight 1<img src="my-imagejpg" /> 123img { box-shadow 0px 25px -10px #666} … paper print light bulb shade grey boring times before… related instagram post muhammad abdull thewilsonthings inspired order make bit translucent technique reflection html˼ss code people comments","text":"Depending on your design, sometimes it is nice to drop a shadow on an image to highlight it: 1<img src="my-image.jpg" /> 123img { box-shadow: 0px 25px 25px -10px #666;} But … it looks like a paper print of the image, with a light bulb in the first third above it. The shade is grey, boring and has been used and seen many times before… An design related Instagram post from Muhammad Abdull of thewilsonthings, inspired me to use the image itself as the shadow in order to make the image look a bit translucent. Should be the same technique as that of a reflection. Here is the HTML/CSS code for it, as the people asking for it in the comments. What we want to achieve is this: Basically, we use a duplicate of the image and position it below the actual image, but slightly offset and blurred. It won’t be a performance issue, as some might think, because it is the very same file and will be loaded only once by the browser. For showing two images in the nearly same place in different layers, we need a wrapper… 1234<div class="image-wrapper"> <img src="my-image.jpg" /> <img class="shadow" src="my-image.jpg" /></div> … and some CSS for positioning the images on top of each other first. Here are the defaults for both image elements: 123456789101112div.image-wrapper { position: relative;}div.image-wrapper img { position: absolute; display: block; top: 0; left: 0; width: 100%; z-index: 1;} Now we have to style the duplicate image that it looks similar to the shadow. We washing it out using the blur filter and the opacity. 1234div.image-wrapper img.shadow { filter: blur(10px); opacity: 0.8;} Last step is to change the duplicates dimensions and the positioning below the original image. We squeeze it by 10% and shift it from left with half of the value back to the center, shift it from top to make it standout at the bottom and send it to the back by taking a lower z-index than the original image. 123456789div.image-wrapper img.shadow { filter: blur(10px); opacity: 0.8; width: 90%; left: 5%; top: 40px; z-index: 0;} Thats it. Here’s a pen to play around with the solution:","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"}]},{"title":"Safely remove multiple classes using a prefix","subtitle":"Avoiding pitfalls when iterating over classList","date":"2021-01-18","updated":"2021-01-18","path":"post/Safely-remove-multiple-classes-using-a-prefix/","permalink":"https://kiko.io/post/Safely-remove-multiple-classes-using-a-prefix/","excerpt":"Writing a Web App with HTML and JavaScript means you deal with several classes on your DOM elements in order to visualize state changes. And there are some pitfalls to be aware of with regard to removal. Assuming you want to open some kind of sidebar above a container. In this sidebar you have several buttons to show different content via JavaScript and a close button, which closes the sidebar again. You HTML code maybe looks like this: 123456789101112131415161718<html> <body> <div id="container">... Main Content ...</div> <nav> <button id="open-sidebar"> </nav> <aside id="sidebar"> <div class="content">... Sidebar Content ...</div> <button id="close">Close Sidebar</button> <button id="content1">Show Content 1</button> <button id="content2">Show Content 2</button> <button id="content3">Show Content 3</button> </aside> </body></html> By clicking on the open-sidebar button, the sidebar is opened and the action, respectively the new state, is vizualized by adding an appropriate class to the parent sidebar element. In order to make it easy for the user, the default content (Content 1) will be loaded also and its state will be marked with another class. 1<aside class="sidebar open open-content1"> A click on of the other content buttons (let’s say Content 2), will replace the current content and the aside classes will change into: 1<aside class="sidebar open open-content2"> Now we want to close the sidebar again, assuming that we don’t have stored the currently opened content in the JavaScript code…","keywords":"writing web app html javascript means deal classes dom elements order visualize state pitfalls aware regard removal assuming open kind sidebar container buttons show content close button closes code 123456789101112131415161718<html> <body> <div id="container"> main </div> <nav> <button id="open-sidebar"> </nav> <aside id="sidebar"> class="content"> id="close">close sidebar</button> id="content1">show 1</button> id="content2">show 2</button> id="content3">show 3</button> </aside> </body></html> clicking open-sidebar opened action vizualized adding class parent element make easy user default loaded marked 1<aside class="sidebar open-content1"> click lets replace current change open-content2"> dont stored code…","text":"Writing a Web App with HTML and JavaScript means you deal with several classes on your DOM elements in order to visualize state changes. And there are some pitfalls to be aware of with regard to removal. Assuming you want to open some kind of sidebar above a container. In this sidebar you have several buttons to show different content via JavaScript and a close button, which closes the sidebar again. You HTML code maybe looks like this: 123456789101112131415161718<html> <body> <div id="container">... Main Content ...</div> <nav> <button id="open-sidebar"> </nav> <aside id="sidebar"> <div class="content">... Sidebar Content ...</div> <button id="close">Close Sidebar</button> <button id="content1">Show Content 1</button> <button id="content2">Show Content 2</button> <button id="content3">Show Content 3</button> </aside> </body></html> By clicking on the open-sidebar button, the sidebar is opened and the action, respectively the new state, is vizualized by adding an appropriate class to the parent sidebar element. In order to make it easy for the user, the default content (Content 1) will be loaded also and its state will be marked with another class. 1<aside class="sidebar open open-content1"> A click on of the other content buttons (let’s say Content 2), will replace the current content and the aside classes will change into: 1<aside class="sidebar open open-content2"> Now we want to close the sidebar again, assuming that we don’t have stored the currently opened content in the JavaScript code… What we have to do, is to iterate over all classes of aside and remove those which starts with open: 12345678910111213141516let sidebar = document.getElementById("sidebar");for (let i = 0; i < sidebar.classList.length; i++) { let value = sidebar.classList[i]; if (value.startsWith("open")) { sidebar.classList.remove(value); }}//orlet sidebar = document.getElementById("sidebar");sidebar.classList.forEach(function(value){ if(value.includes("open")) { sidebar.classList.remove(value); };}); Both approaches have a pitfall: when the first class, starting with open, is removed from the list, the length of the classList array changes immediatly and we won’t reach the last class in the list … ! The solution is to find and remove all appropriate classes at once, for example by using RegEx and a reusable helper function: 12345678910function removeClassByPrefix(el, prefix) { let pattern = '(' + prefix + '(\\\\s|(-)?(\\\\w*)(\\\\s)?)).*?'; var regEx = new RegExp(pattern, 'g'); el.className = el.className.replace(regEx, '');}//...let sidebar = document.getElementById("sidebar");removeClassByPrefix(sidebar, "open"); Update, 24 Jan 2021The first posted RegEx pattern didn’t worked properly, because it has found the prefix only and not the whole word, so I have updated the pattern. You can try it out at RegExr.com - Remove Class By Prefix.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"DOM","slug":"DOM","permalink":"https://kiko.io/tags/DOM/"}]},{"title":"Use and manage multiple Node.js versions on Windows 10","subtitle":null,"date":"2021-01-08","updated":"2021-01-08","path":"post/Use-and-manage-multiple-Node-js-versions-on-Windows-10/","permalink":"https://kiko.io/post/Use-and-manage-multiple-Node-js-versions-on-Windows-10/","excerpt":"For a new project I needed to have Node 14 running on my Windows 10 machine, so installation was done quickly via downloading and running the setup file. A short time later I wanted to write a new blog post here on kiko.io, which depends on the Node.js based static site generator Hexo … and ran into several problems. First of all my hero image processing script (see Automatic Header Images in Hexo) returned an exception. The script uses hexo-fs and the problem is known quite some time, according to this Github issue. The guys recommend to downgrade to an older version of Node.js … :( Ok … I needed a solution to install multiple Node.js versions and switch between them, depending on which project I want to work on … and there is one: nvm-windows by Corey Butler!","keywords":"project needed node running windows machine installation quickly downloading setup file short time wanted write blog post kikoio depends nodejs based static site generator hexo … ran problems hero image processing script automatic header images returned exception hexo-fs problem github issue guys recommend downgrade older version solution install multiple versions switch depending work nvm-windows corey butler","text":"For a new project I needed to have Node 14 running on my Windows 10 machine, so installation was done quickly via downloading and running the setup file. A short time later I wanted to write a new blog post here on kiko.io, which depends on the Node.js based static site generator Hexo … and ran into several problems. First of all my hero image processing script (see Automatic Header Images in Hexo) returned an exception. The script uses hexo-fs and the problem is known quite some time, according to this Github issue. The guys recommend to downgrade to an older version of Node.js … :( Ok … I needed a solution to install multiple Node.js versions and switch between them, depending on which project I want to work on … and there is one: nvm-windows by Corey Butler! This Node.js Version Manager for Windows is working similar to the often mentioned n and nvm, which support Linux and Mac only. The latest release of nvm-windows can be downloaded here. The setup is pretty straight forward and asks you at the very end, if the currently installed Node.js version should be managed by it. Confirmed… There are just a few commands to know and to run in the command line: List available Node.js versions 1nvm list available Install needed Node.js version 1nvm install <version> Switch to particular Node.js version 1nvm use <version> List all installed Node.js versions 1nvm ls In case you have Node.js version dependend utilities installed globally, you need to run npm install -g after switch. More Info Github: coreybutler/nvm-windowsMicrosoft Docs: Set up your Node.js development environment directly on Windows","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Node.js","slug":"Node-js","permalink":"https://kiko.io/tags/Node-js/"}]},{"title":"How to prevent duplicate events","subtitle":null,"date":"2021-01-07","updated":"2021-01-07","path":"post/How-to-prevent-duplicate-events/","permalink":"https://kiko.io/post/How-to-prevent-duplicate-events/","excerpt":"I’m working on a new web app that contains a sliding out panel with some additional information on the selected element. This panel can be closed by the user via the ESC key. The implementation on initializing the panel seems very straight forward: 1234567891011class Panel() { init() { document.addEventListener("keydown", function(event) { if(event.key === "Escape"){ //close the panel } }); }} Problem is: the panel, which is part of the basic HTML, will be initialized with its content and functionality in a ES6 class. So … on every init, another event listener is added. You can easily figure that out, by calling getEventListeners(document) in the Chrome DevTools:","keywords":"im working web app sliding panel additional information selected element closed user esc key implementation initializing straight forward 1234567891011class { init documentaddeventlistener"keydown" functionevent ifeventkey === "escape"{ //close } }} problem part basic html initialized content functionality es6 class … event listener added easily figure calling geteventlistenersdocument chrome devtools","text":"I’m working on a new web app that contains a sliding out panel with some additional information on the selected element. This panel can be closed by the user via the ESC key. The implementation on initializing the panel seems very straight forward: 1234567891011class Panel() { init() { document.addEventListener("keydown", function(event) { if(event.key === "Escape"){ //close the panel } }); }} Problem is: the panel, which is part of the basic HTML, will be initialized with its content and functionality in a ES6 class. So … on every init, another event listener is added. You can easily figure that out, by calling getEventListeners(document) in the Chrome DevTools: As there is no way in JS to find and replace the event which was previously added, we have to remove the existing event by using removeEventListener and add it again. Most important parameter on removing is the instance of the event handler, which was used the add the event previously. 12345678910111213class Panel() { init() { function onEscapeKey (event) { if(event.key === "Escape"){ //close the panel } } document.removeEventListener("keydown", onEscapeKey); document.addEventListener("keydown", onEscapeKey); }} But … as the handler onEscapeKey is defined in a class, every time a new instance of the class is created, the handler will be not the same as the previous one! We have to store the event handler globally… 12345678910111213class Panel() { init() { document.removeEventListener("keydown", window.panelEscapeKeyHandler); window.panelEscapeKeyHandler = function onEscapeKey(event) { if(event.key === "Escape"){ //close the panel } }; document.addEventListener("keydown", window.panelEscapeKeyHandler); }} Works!","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Events","slug":"Events","permalink":"https://kiko.io/tags/Events/"}]},{"title":"Utilize a repository of reusable ES6 template literals","subtitle":null,"date":"2021-01-03","updated":"2021-01-03","path":"post/Utilize-a-repository-of-reusable-ES6-template-literals/","permalink":"https://kiko.io/post/Utilize-a-repository-of-reusable-ES6-template-literals/","excerpt":"The Template Literals introduced with ES6 are very useful to deal with multiline strings, because they support embedded expressions. Gone are the days of endless string concatination or replacing variables in a string by using RegEx. Instead of… 12345678var url = ...var file = ...var template = '<div class="photo">' + '<a href="' + url + "' + 'style="background-image: url(' + file + ')"</a>' + '</div>' … you can write: 123456789var url = ...var file = ...var template = ` <div class="photo"> <a href="${url}/" style="background-image: url(${file});"></a> </div>`, It’s much cleaner and easier to handle, as you can copy your needed HTML right into your code and surround it by backtick (!) characters. Insert your variable placeholders (expressions), indicated by a dollar sign and curly braces, and you are done. But there is one “restriction”, you have to be aware of: the interpolation (substitution of the expressions) is done at declaration time and not at runtime. You can’t define your literals seperatly, take one and make your substitution as you need it, like you would do with Handlebars or other templating engines. Therefore the name template literals is a bit misleading. But … there is a way to achieve this anyway…","keywords":"template literals introduced es6 deal multiline strings support embedded expressions days endless string concatination replacing variables regex of… 12345678var url = var file '<div class="photo">' + '<a href="' "' 'style="background-image url' '"</a>' '</div>' … write 123456789var ` <div class="photo"> <a href="${url}/" style="background-image url${file}"></a> </div>` cleaner easier handle copy needed html code surround backtick characters insert variable placeholders dollar sign curly braces restriction aware interpolation substitution declaration time runtime define seperatly make handlebars templating engines bit misleading achieve anyway…","text":"The Template Literals introduced with ES6 are very useful to deal with multiline strings, because they support embedded expressions. Gone are the days of endless string concatination or replacing variables in a string by using RegEx. Instead of… 12345678var url = ...var file = ...var template = '<div class="photo">' + '<a href="' + url + "' + 'style="background-image: url(' + file + ')"</a>' + '</div>' … you can write: 123456789var url = ...var file = ...var template = ` <div class="photo"> <a href="${url}/" style="background-image: url(${file});"></a> </div>`, It’s much cleaner and easier to handle, as you can copy your needed HTML right into your code and surround it by backtick (!) characters. Insert your variable placeholders (expressions), indicated by a dollar sign and curly braces, and you are done. But there is one “restriction”, you have to be aware of: the interpolation (substitution of the expressions) is done at declaration time and not at runtime. You can’t define your literals seperatly, take one and make your substitution as you need it, like you would do with Handlebars or other templating engines. Therefore the name template literals is a bit misleading. But … there is a way to achieve this anyway… Tagged TemplatesBeside Template Literals, ES6 introduced Tagged Templates (exact: Tagged Template Literals). These tags are functions, which allows you to parse a Template Literal. Definition is like this: 123function myTag(literals, ...expressions) { //do the substitution and return a string} You can use these tags by prefixing you literal: 1myTag`Hello ${firstName} ${lastName}!` Using Tagged Templates to build a template repository would mean, you have to write one tag function for every template … doable, but time consuming. Dynamic Tag FunctionTo avoid this, we can write a universal tag function, which utilizes the Function constructor, to create the tag function dynamically: 12345678function fillTemplate(templateString, templateVars) { var func = new Function( ...Object.keys(templateVars), "return `" + templateString + "`;") return func(...Object.values(templateVars));} Don’t use this approach on user inputs as expressions, to avoid XSS! Let’s see an example…Given is a tiny web app with the following structure: index.html123456789101112<!DOCTYPE html><html> <head> <title>Reusable ES6 template literals</title> <meta charset="UTF-8" /> <link rel="stylesheet" href="/src/style.css"> </head> <body> <main id="main"></main> <script src="src/index.js"></script> </body></html> index.js12345import { App } from "./app.js";const app = new App();app.init(); app.js123456class App { init() { //do something }}export { App }; What we want to do now, is to load some images into the main element, by using a more or less complex element structure: 1234<div class="photo"> <a href="<!-- Url to view the photo -->" style="background-image: url(<!-- Url of the photo file -->);"></a></div> To separate our templates from the main code, we create a template module, which contains the dynamic tag function from above and a photo template we want to use in our app template.js12345678910111213141516171819202122232425class Templates { //Template photo(data) { return this.fillTemplate( ` <div class="photo"> <a href="${data.url}/" style="background-image: url(${data.file});"></a> </div> `, data ); } //Dynamic Tag Function fillTemplate(templateString, templateVars) { var func = new Function(...Object.keys(templateVars), "return `" + templateString + "`;" ); return func(...Object.values(templateVars)); } }export { Templates }; The template retrieves a data object, with the values of the defined expressions, and calls the dynamic tag function on the literal template. This we can use now in our app code: app.js12345678910111213141516171819202122//Import Template moduleimport { Templates } from "./templates.js";class App { init() { //Initialize Templates this._templates = new Templates(); //Insert photo into MAIN element let main = document.getElementById("main"); main.insertAdjacentHTML( "beforeend", this._templates.photo({ file: "my-photo.jpg", url: "https://link-to-my.photo.com" }) ); }}export { App }; See it live at codesandbox.io. More Info Stackoverflow: Can ES6 template literals be substituted at runtime (or reused)?Github/Adelphos: ES6-Reuseable-Template","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"ES6","slug":"ES6","permalink":"https://kiko.io/tags/ES6/"},{"name":"Templating","slug":"Templating","permalink":"https://kiko.io/tags/Templating/"}]},{"title":"Spice Up Windows Terminal","subtitle":"How to make Powershell a little prettier","date":"2020-12-24","updated":"2021-04-16","path":"post/Spice-Up-Windows-Terminal/","permalink":"https://kiko.io/post/Spice-Up-Windows-Terminal/","excerpt":"Working with the PowerShell in 2020 means fun, because of the new Windows Terminal (get it from Windows Store). It has more power as the old Powershell Console and it is visually adaptable to your personal taste, by installing the wonderful theming engine oh-my-posh from Jan De Dobbeleer. To get Oh-My-Posh properly run, there are several steps to do I want to show here in a nutshell…","keywords":"working powershell means fun windows terminal store power console visually adaptable personal taste installing wonderful theming engine oh-my-posh jan de dobbeleer properly run steps show nutshell…","text":"Working with the PowerShell in 2020 means fun, because of the new Windows Terminal (get it from Windows Store). It has more power as the old Powershell Console and it is visually adaptable to your personal taste, by installing the wonderful theming engine oh-my-posh from Jan De Dobbeleer. To get Oh-My-Posh properly run, there are several steps to do I want to show here in a nutshell… Step 1 - Install a suitable fontAs the theming engine uses Powerline glyphs, you need to install a font which support them, for example the new Cascadia Code PL from Microsoft. Download, unzip and install the OTF and/or TTF font files via context menu in your Windows Explorer. Step 2 - Set new font in your settingsOpen up you Terminal settings… … and add following new line to the PowerShell section: 1"fontFace": "Cascadia Code PL", Step 3 - Install oh-my-poshFollowing PowerShell command installs the theming engine itself: 1Install-Module oh-my-posh -Scope CurrentUser If you want to display Git status information also, run this command: 1Install-Module posh-git -Scope CurrentUser To let the command-line editing environment to be customized install PSReadline: 1Install-Module -Name PSReadLine -Scope CurrentUser -Force -SkipPublisherCheck Step 4 - Load on startupIn order to load the theming engine in every new terminal window, edit your PowerShell profile by opening it up with the command … 1notepad $PROFILE and add following lines in the upcoming text file: 123Import-Module oh-my-poshImport-Module posh-gitSet-Theme Paradox Paradox is one of 27 themes available. You will find all themes in your DOCUMENTS folder under ..\\WindowsPowerShell\\Modules\\oh-my-posh\\<version>\\Themes and some visual representations at https://github.com/JanDeDobbeleer/oh-my-posh?#themes. #Update, April 2021Scott Hanselman has mentioned a new improvement recently: Show suitable icons on listing files: Download and install CaskaydiaCove Nerd Font at https://www.nerdfonts.com/font-downloads Open Terminal Settings (like in Step 2) Replace the fontface with "CaskaydiaCove Nerd Font" Run Install-Module -Name Terminal-Icons -Repository PSGallery in Terminal, opened as administrator Add Import-Module -Name Terminal-Icons in your profile (like in Step 4) You will get this on calling dir, for example: More Info Windows Store: Windows TerminalMicrosoft: Cascadia Code PLGitHub: JanDeDobbeleer/oh-my-poshMicrosoft Docs: Set up Powerline in Windows TerminalScott Hanselman: How to make a pretty prompt in Windows Terminal with Powerline, Nerd Fonts, Cascadia Code, WSL, and oh-my-poshScott Hanselman: Taking your PowerShell prompt to the next level with Windows Terminal and Oh my Posh 3Scott Hanselman: Take your Windows Terminal and PowerShell to the next level with Terminal Icons","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Theming","slug":"Theming","permalink":"https://kiko.io/tags/Theming/"},{"name":"PowerShell","slug":"PowerShell","permalink":"https://kiko.io/tags/PowerShell/"}]},{"title":"Discoveries #5","subtitle":null,"series":"Discoveries","date":"2020-12-19","updated":"2020-12-19","path":"post/Discoveries-5/","permalink":"https://kiko.io/post/Discoveries-5/","excerpt":"In this episode of the Discoveries (almost) everything is about images and the web. There are so many pitfalls to do it wrong, but many more possibilities to do it right, especially with these resources I found in the last few weeks. ASP.NET Core Image Resizing MiddlewareBest way to lazy load images for maximum performanceimage orientation on the webcosha - Colorful shadows for your imagesparax-bg - Parallax Backgroundsparax - Parallax ElementsLuminous LightboxTiny-Swiper - Image Carousel","keywords":"episode discoveries images web pitfalls wrong possibilities resources found weeks aspnet core image resizing middlewarebest lazy load maximum performanceimage orientation webcosha - colorful shadows imagesparax-bg parallax backgroundsparax elementsluminous lightboxtiny-swiper carousel","text":"In this episode of the Discoveries (almost) everything is about images and the web. There are so many pitfalls to do it wrong, but many more possibilities to do it right, especially with these resources I found in the last few weeks. ASP.NET Core Image Resizing MiddlewareBest way to lazy load images for maximum performanceimage orientation on the webcosha - Colorful shadows for your imagesparax-bg - Parallax Backgroundsparax - Parallax ElementsLuminous LightboxTiny-Swiper - Image Carousel ASP.NET Core Image Resizing Middleware by Jeremy Paddison https://www.paddo.org/asp-net-core-image-resizing-middleware/ Jeremy shows in his blog post the possibilities of dealing with images in ASP.NET Core in terms of format, orientation and quality. A must read for every .NET developer. Best way to lazy load images for maximum performance by Adrian Bece https://blog.prototyp.digital/best-way-to-lazy-load-images-for-maximum-performance/ Delivering images on the web is difficult due to different devices and bandwidths. Adrian shows how to achieve a maximum of performance on lazy loading images via native JavaScript. image orientation on the web by Michael Scharnagl https://justmarkup.com/articles/2019-10-21-image-orientation/ Automatic uploading and viewing images fails sometimes on portrait shots, because of misintrepretated orientation information by the different browsers. Michael adresses this problem with a Node.JS solution. cosha - Colorful shadows for your images by Robin Löffel https://github.com/robinloeffel/cosha Adding a blurry shadow under an image to let the photo stand out from the background, is a nice technique to draw the users attention. Robin goes one step further with his JavaScript solution on colorful shadows, which represents the colors of the image. parax-bg - Parallax Backgrounds by Tobias Buschor https://github.com/nuxodin/parax-bg Parallaxing backgrounds are fairly attracting and so it is with Tobias’ approach on that for developers: easy to use and fast. parax - Parallax Elements by Tobias Buschor https://github.com/nuxodin/parax If you just want to parallax some elements instead of backgrounds, Tobias has also a solution for that: Parax. Luminous Lightbox by imgix https://github.com/imgix/luminous There are tons of image lightboxes out there and here is another one: Luminous from imgx. It is my favourite and I use it in this blog. Its lightweight and easy to use, for the user and the developer. A pearl… Tiny-Swiper - Image Carousel by Joe Harris https://tiny-swiper.joe223.com/docs/demo/ The same applies to image carousels, but Tiny-Swiper is here outstanding too. It is simple, but powerful and well documented. A must use…","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Indian Presets for Lightroom","subtitle":null,"series":"Lightroom Presets","date":"2020-10-28","updated":"2020-10-28","path":"post/Indian-Presets-for-Lightroom/","permalink":"https://kiko.io/post/Indian-Presets-for-Lightroom/","excerpt":"In 2019 I was on a short, stressful business trip to Dehli, India and one night we had the opportunity to relax a bit by driving around the city and visit some beautiful places of interest. I had no gear at all, just my Sony smartphone, but it is remarkable how good this worked out.","keywords":"short stressful business trip dehli india night opportunity relax bit driving city visit beautiful places interest gear sony smartphone remarkable good worked","text":"In 2019 I was on a short, stressful business trip to Dehli, India and one night we had the opportunity to relax a bit by driving around the city and visit some beautiful places of interest. I had no gear at all, just my Sony smartphone, but it is remarkable how good this worked out. Indian Sunset var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-sswvls\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-mpyd35\"), { controlColor: themeColor, controlShadow: false, verticalMode: true, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Indian Sunset.xmp","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Presets","slug":"Presets","permalink":"https://kiko.io/tags/Presets/"}]},{"title":"Israeli Presets for Lightroom","subtitle":null,"series":"Lightroom Presets","date":"2020-10-27","updated":"2020-10-27","path":"post/Israeli-Presets-for-Lightroom/","permalink":"https://kiko.io/post/Israeli-Presets-for-Lightroom/","excerpt":"I’m a travel and event photo enthusiast, which means I’m shooting a lot of photographs on vacation or at special events only a few times a year. After I’m back home and start the image processing, I develop a particular look for my images of the past vacation or event. This has a lot to do with my mood and is very intuitive. Not all images are the same in terms of composition and light and so I create usually 3 or 4 different presets each time during image processing. Back in 2019, I was traveling around Israel, a fascinating country where almost every wall has a story to tell and I was listening through my viewfinder. Here I want to share the presets with you…","keywords":"im travel event photo enthusiast means shooting lot photographs vacation special events times year back home start image processing develop images past mood intuitive terms composition light create presets time traveling israel fascinating country wall story listening viewfinder share you…","text":"I’m a travel and event photo enthusiast, which means I’m shooting a lot of photographs on vacation or at special events only a few times a year. After I’m back home and start the image processing, I develop a particular look for my images of the past vacation or event. This has a lot to do with my mood and is very intuitive. Not all images are the same in terms of composition and light and so I create usually 3 or 4 different presets each time during image processing. Back in 2019, I was traveling around Israel, a fascinating country where almost every wall has a story to tell and I was listening through my viewfinder. Here I want to share the presets with you… Israeli ColorsThe mediaval walls of Jaffa glow in an inimitable way and brings other colors to shine the same way. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-pc7vzi\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Israeli Colors.xmp Israeli LightsThe light in the eastern Mediterranean is stunning. The warm tone of the sand and the turquoise color of the water had to pop out. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-m51qz4\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Israeli Lights.xmp Israeli DramaA visit of Yad Vashem moved me a lot and this preset is a expression of that. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-mvvyeq\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Israeli Drama.xmp Israeli Near BlackIf you think of the tourists away, Jerusalem takes you to another level because of its age and history and nothing fits more to that than the sepia look of old pictures. var themeColor = \"#ffffff\"; if (localStorage.getItem(\"theme\") === 'dark') { themeColor = \"#222222\" } new ImageCompare(document.getElementById(\"image-compare-cxg116\"), { controlColor: themeColor, controlShadow: false, verticalMode: false, showLabels: true, labelOptions: { before: 'Original', after: 'Preset', onHover: true, } }).mount(); Download Lightroom Preset Israeli Near Black.xmp","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Presets","slug":"Presets","permalink":"https://kiko.io/tags/Presets/"}]},{"title":"Folder based publishing in Lightroom","subtitle":null,"date":"2020-10-26","updated":"2020-10-26","path":"post/Folder-based-publishing-in-Lightroom/","permalink":"https://kiko.io/post/Folder-based-publishing-in-Lightroom/","excerpt":"In all times photography was a process: First you shoot you images, then you edit them and in the third step you publish them elsewhere. Today Adobe Lightroom is a de-facto standard in photo processing, especially when you shoot RAW images. And I mean Lightroom Classic and not the new web/smartphone based software, which doesn’t come even close to the desktop application yet. I use Lightroom for all purposes after I shot my images: editing, cataloguing, managing and … publishing. On publishing Lightroom offers you two approaches: Export and Publish. While Export is mainly for creating JPG copies of edited RAW images, Publish goes one step further and gives you the ability to do “something” with your exported JPG’s, for example upload them on Flickr, Instagram, 500px or to your own web server via FTP. Major difference to Export is, that Publish keeps your images in sync. Everytime you change the source images, the defined publish services recognizes and offer you to re-publish your image. In the past years I tried a lot of Lightroom plugins for publishing on several platforms, but it doesn’t work out for long, because all these platforms change their API almost every year (or are stamped) and the sparely maintained 3rd party plugins break. My workflow for quite some time is to publish my photos on the hard drive, in a folder, which is synced via Dropbox with the cloud. From there I distribute them further.","keywords":"times photography process shoot images edit step publish today adobe lightroom de-facto standard photo processing raw classic web/smartphone based software doesnt close desktop application purposes shot editing cataloguing managing … publishing offers approaches export creating jpg copies edited ability exported jpgs upload flickr instagram 500px web server ftp major difference sync everytime change source defined services recognizes offer re-publish image past years lot plugins platforms work long api year stamped sparely maintained 3rd party break workflow time photos hard drive folder synced dropbox cloud distribute","text":"In all times photography was a process: First you shoot you images, then you edit them and in the third step you publish them elsewhere. Today Adobe Lightroom is a de-facto standard in photo processing, especially when you shoot RAW images. And I mean Lightroom Classic and not the new web/smartphone based software, which doesn’t come even close to the desktop application yet. I use Lightroom for all purposes after I shot my images: editing, cataloguing, managing and … publishing. On publishing Lightroom offers you two approaches: Export and Publish. While Export is mainly for creating JPG copies of edited RAW images, Publish goes one step further and gives you the ability to do “something” with your exported JPG’s, for example upload them on Flickr, Instagram, 500px or to your own web server via FTP. Major difference to Export is, that Publish keeps your images in sync. Everytime you change the source images, the defined publish services recognizes and offer you to re-publish your image. In the past years I tried a lot of Lightroom plugins for publishing on several platforms, but it doesn’t work out for long, because all these platforms change their API almost every year (or are stamped) and the sparely maintained 3rd party plugins break. My workflow for quite some time is to publish my photos on the hard drive, in a folder, which is synced via Dropbox with the cloud. From there I distribute them further. Important on this approach is, to have a coherent output folder structure in order find a particular image afterwards. I store my RAW images on an external hard drive in a structure like this: 1234<Drive:\\> -> Fotos -> <Year> -> <Year>-<Month> <Eventname> First step after shooting is to copy all RAW files from the SD Card into a new subfolder of the current year. From there I import them into my Lightroom catalog. After sorting, rejecting, editing and flagging in Lightroom, I have to “export” the 4- and 5-star rated images into the cloud. There I have a slightly different structure, with a different root folder name and without the year: 123<Dropbox> -> Photos -> <Year>-<Month> <Eventname> With the built-in “Hard Drive” publish service of Lightroom, it is feasible to “export” the images, but not in my wanted folder structure, because it is not possible to use the sources’ folder name as output folder name by option. It has to be specified manually each time, which is not very comfortable. Plugin to the rescueJeffrey Friedl, who is in the Lightroom plugin business about a decade, offers two plugins, which can solve the problem easily: Jeffrey’s “Folder Publisher“ Lightroom Plugin Exports to disk in a folder hierarchy that mimics the folder hierarchy in your Lightroom catalog This one helps me not much, because my output folder would look like this: 12345<Dropbox> -> Photos -> Fotos -> <Year> -> <Year>-<Month> <Eventname> Jeffrey’s “Collection Publisher” Lightroom PluginExports to local disk in a folder hierarchy that mimics the collection hierarchy you build within Lightroom Besides the commonality of defining a root folder, where the files are exported to, Jeffrey’s plugin has much more options to improve the export. As the main feature, you can create collections within the plugin to define the export targets: In the options of these collections, you can set several templates (variables) to let the plugin name the export subfolder automatically: By using Smart Collections you don’t even need to drag & drop your images to publish to the publish collection. Lightroom will do this auto-magically.","categories":[{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"}],"tags":[{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"}]},{"title":"Discoveries #4","subtitle":null,"series":"Discoveries","date":"2020-10-10","updated":"2020-10-10","path":"post/Discoveries-4/","permalink":"https://kiko.io/post/Discoveries-4/","excerpt":"It is so amazing how many cool stuff developers around the world are producing these days. Or they do what they always do, but I have more time to read about their smart ideas and solutions. This month I have 8 pearls for you: waitForElementTransition()Using Flexbox and text ellipsis togetherUsing Trello as a Super Simple CMSMemorize Scroll Position Across Page LoadsA free guide to HTML5 <head> elementsBVSelect - Vanilla JSA clock that represents the time as hex color valuesAnimate.css - Just-add-water CSS animations","keywords":"amazing cool stuff developers world producing days time read smart ideas solutions month pearls waitforelementtransitionusing flexbox text ellipsis togetherusing trello super simple cmsmemorize scroll position page loadsa free guide html5 <head> elementsbvselect - vanilla jsa clock represents hex color valuesanimatecss just-add-water css animations","text":"It is so amazing how many cool stuff developers around the world are producing these days. Or they do what they always do, but I have more time to read about their smart ideas and solutions. This month I have 8 pearls for you: waitForElementTransition()Using Flexbox and text ellipsis togetherUsing Trello as a Super Simple CMSMemorize Scroll Position Across Page LoadsA free guide to HTML5 <head> elementsBVSelect - Vanilla JSA clock that represents the time as hex color valuesAnimate.css - Just-add-water CSS animations waitForElementTransition() by Mark Kennedy https://github.com/mkay581/wait-for-element-transition In these days a good UI doesn’t do without some animations or transitions and it is always advisible to use CSS for it, if possible. Marks shows us with his solution, how to wait in JavaScript for a transition to finish, before we continue to do something else in JS. Using Flexbox and text ellipsis together by Leonardo Faria https://leonardofaria.net/2020/07/18/using-flexbox-and-text-ellipsis-together/ In case you offer downloads with very long file names from time to time, you might use CSS’s ellipsis to cut it down. But you always loose the last three chars, the file extension. Leonardo show us, how to avoid that, by using a clever mix of ellipsis and flexbox. Using Trello as a Super Simple CMS by Phil Hawksworth https://css-tricks.com/using-trello-as-a-super-simple-cms As I love Trello and use it daily, among others as a reading list (see Add website to Trello card the better way), I can’t wait to try Phils approach to process Trello boards automatically. Memorize Scroll Position Across Page Loads by Chris Coyier https://css-tricks.com/memorize-scroll-position-across-page-loads/ Chris shows Hakim El Hattab’s trick, how to store the current scroll position and restore it when user comes back. Simple, but a gain in usability. A free guide to HTML5 <head> elements by Josh Buchea https://htmlhead.dev Doing your Web Developer job right, means you have to be aware of the META tags in your HTML. htmlhead.dev is a good reference, because it lists and describes mostly all known META tags. BVSelect - Vanilla JS by Bruno Vieira https://bmsvieira.github.io/BVSelect-VanillaJS There are many HTML/Javascript driven dropdowns out there. So has Bruno, but his solution don’t even looks nice, it is written in ES6, has no dependencies and is dead simple to use. A clock that represents the time as hex color values by Jamel Hammoud https://github.com/JamelHammoud/hextime The time is shown mostly as a six digit number … Hours, Minutes and Seconds, with a leading 0. Color Hex codes have also 6 digits and Jamel the idea to bring both together… Animate.css - Just-add-water CSS animations by Daniel Eden https://animate.style Daniel and his buddies offers an Stylesheet with dozens of cool and easy to use text animations.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Implement source switch for SPA","subtitle":"Asynchronous loading of JS and CSS depending on the environment","date":"2020-10-04","updated":"2020-10-04","path":"post/Implement-source-switch-for-SPA/","permalink":"https://kiko.io/post/Implement-source-switch-for-SPA/","excerpt":"A while ago I wrote a Single Page Application (SPA) with jQuery and and decided to use some useful plugins to avoid reinventing the wheel. To keep the delivered sources small, I used the bundler Gulp, to pack all JS plugins in a single file and another one for my custom JS code. I used the same procedure with the CSS files. The SPA contained only a single HTML file in which all bundeled sources and needed HTML template blocks were included, in order to load most of the stuff while starting the app, when the users sees a GMail-like loading screen. But the whole thing had one disadvantage: Debugging for example in Chrome Dev Tool is not a joy, if the code is packed with Gulp Concat and Gulp Uglify. It would be much more convenient, if the source loading can be done depending on the environment.","keywords":"ago wrote single page application spa jquery decided plugins avoid reinventing wheel delivered sources small bundler gulp pack js file custom code procedure css files contained html bundeled needed template blocks included order load stuff starting app users sees gmail-like loading screen thing disadvantage debugging chrome dev tool joy packed concat uglify convenient source depending environment","text":"A while ago I wrote a Single Page Application (SPA) with jQuery and and decided to use some useful plugins to avoid reinventing the wheel. To keep the delivered sources small, I used the bundler Gulp, to pack all JS plugins in a single file and another one for my custom JS code. I used the same procedure with the CSS files. The SPA contained only a single HTML file in which all bundeled sources and needed HTML template blocks were included, in order to load most of the stuff while starting the app, when the users sees a GMail-like loading screen. But the whole thing had one disadvantage: Debugging for example in Chrome Dev Tool is not a joy, if the code is packed with Gulp Concat and Gulp Uglify. It would be much more convenient, if the source loading can be done depending on the environment. First step was to replace the SCRIPT and LINK tags in die index.html with a dynamic loading approach using JavaScript. Dynamic JS loadingFor some custom code it was necessary to load the plugins previously, because of dependencies. 123456789101112131415161718function addScriptAsync(url) { return new Promise(function(resolve, reject) { var script = document.createElement("script"); script.type = "text/javascript"; script.src = url; script.addEventListener("load", function() { resolve(script); }, false); script.addEventListener("error", function() { reject(script); }, false); document.getElementsByTagName('head')[0].appendChild(script); });} By returning a Promise, the calling code is able to wait for a dependent source to load: 123addScriptAsync("Build/vendor.min.js").then(function() { addScriptAsync("Build/custom.min.js");}); Dynamic CSS loadingLoading CSS is pretty straightforward and includes an id as parameter, in order to be able to access the style afterwards, for example when tehh user is chanhing the SPA’s theme: 12345678910function addStylesheet(url, id) { var stylesheet = document.createElement('link'); stylesheet.rel = 'stylesheet'; stylesheet.type = 'text/css'; stylesheet.href = url; if (id) { stylesheet.setAttribute("id", id); } document.getElementsByTagName('head')[0].appendChild(stylesheet);} 12addStylesheet("Build/vendor.css");addStylesheet("Build/custom.css"); Consider the environmentNow everything was set up to implement a switch, depending on whether the SPA was started locally or in production. 12345678910111213141516171819202122var _DEV = (window.location.hostname.indexOf("localhost") !== -1);addStylesheet("Build/vendor.css");if (_DEV) { addStylesheet("Libraries/styles.css"); addStylesheet("Libraries/helpers.css"); ...} else { addStylesheet("Build/custom.css");}addScriptAsync("Build/vendor.min.js").then(function() { if (_DEV) { addScriptAsync("Libraries/prototypes.js") .then(function() { return addScriptAsync("Libraries/tools.js"); }) .then(function() { return addScriptAsync("Libraries/app.js"); }) ... } else { return addScriptAsync("Build/custom.min.js"); }})","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"SPA","slug":"SPA","permalink":"https://kiko.io/tags/SPA/"},{"name":"Bundling","slug":"Bundling","permalink":"https://kiko.io/tags/Bundling/"}]},{"title":"Show related posts in Hexo","subtitle":null,"series":"A New Blog","date":"2020-10-03","updated":"2020-10-03","path":"post/Show-related-posts-in-Hexo/","permalink":"https://kiko.io/post/Show-related-posts-in-Hexo/","excerpt":"It is always nice to point the readers of your blog’s articles to related posts, they might be interested in. They stay a little longer to understand what you have to offer and increases the likelihood that they become loyal readers, followers or subscribers. Related posts has become a standard on delivering news and posts. In the default Hexo theme Landscape, on which this blog is based, there is no such function built in, but as the Hexo community is very busy, there are some plugins you can use.","keywords":"nice point readers blogs articles related posts interested stay longer understand offer increases likelihood loyal followers subscribers standard delivering news default hexo theme landscape blog based function built community busy plugins","text":"It is always nice to point the readers of your blog’s articles to related posts, they might be interested in. They stay a little longer to understand what you have to offer and increases the likelihood that they become loyal readers, followers or subscribers. Related posts has become a standard on delivering news and posts. In the default Hexo theme Landscape, on which this blog is based, there is no such function built in, but as the Hexo community is very busy, there are some plugins you can use. Plugin: hexo-list-related-postsThis plugin, available at GitHub is pretty lean and generates a list of links to related posts based on tags. It just counts how often a tag is occuring and shows a list of related posts either by count descending or randomly. Advantage: Easy and fast Disadvantage: Necessity of a sophisticated tag system Technical approach Plugin: hexo-related-postsSergey Zwezdin made much more effort in his solution. The plugins depends on statistic methodologies like Stemming and TF/IDF, provided by the Node library Natural. It has plenty setting options like weighting and reserved words in order to optimize results. Advantages: Much better results Disadvantages: Huge installation, because of many dependent Node modules Necessity of maintaining reserved words Technical approach Manually CuratedOne point, that no technical solution can achieve is: you can guide the reader through your blog, by pointing out posts, which doesn’t really belong to the topic, but tries to give him a wider perspective on your thoughts or work. This is only possible, if you link the related posts manually. Here is a way to implement the requirements… The right place to store related posts is in the Frontmatter of your article. Create a list below the keyword related and take the slug (name of the post file) of the posts you want to show below the article as entries: 12345title: My New fancy Postrelated: - my-other-post - one-of-my-first-posts - yet-another-post In your article.ejs add a new partial called related to the place where it should be shown under the content of the actual article: 123456789101112131415161718192021<article id="<%= post.layout %>-<%= post.slug %>" class="article article-type-<%= post.layout %>" itemscope itemprop="blogPost"> ... <div class="article-inner"> <%- post.content %> </div> <% if (!index){ %> <!-- NEW RELATED PARTIAL --> <%- partial('post/related') %> <%- partial('post/comments') %> <%- partial('post/nav') %> <% } %></article> In the folder themes/landscape/layout/_partial/post, where all partials are stored which belongs to posts, create the new partial file: related.ejs12345678910111213141516171819202122232425<% if (post.related && post.related.length){ %> <div class="article-related"> <h2>Related</h2> <div class="archives"> <!-- Loop through the Frontmatter list of RELATED posts --> <% post.related.forEach(function(item) { %> <!--Determine the post(s) with the given slug --> <% var posts = site.posts.filter(function(post) { return post.slug.toLowerCase() === item.toLowerCase(); }); %> <!-- Loop through the post(s) and render the archive panel --> <% posts.each(function(post) { %> <%- partial('../archive-post', { post: post, show_link: true }) %> <% }); %> <% }); %> </div> </div><% } %> (Remove the comments, because they doesn’t belong to EJS) In this partial we loop through the Frontmatter list of related posts, determine the post by the given slug and render an archive panel for each post. The list site.posts should always contain a slug just once, therefore getting an array of posts and looping is just a precuation. What you are getting you can see below…","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"Discoveries #3 - Tutorials","subtitle":null,"series":"Discoveries","date":"2020-09-29","updated":"2020-09-29","path":"post/Discoveries-3-Tutorials/","permalink":"https://kiko.io/post/Discoveries-3-Tutorials/","excerpt":"Some articles I stumble upon in my daily routine of reading news and blogs are diving very deep in a certain topic, especially if they are describing the basics of techniques I use every day. All of the following reading tips are of the type “ahh, that’s why this works like that” or “uuh, I just scatch on the surface on that”. Take your time and read the articles in detail. We all never stop learning and it’s a pleasure to do so… CSS CSS Viewport UnitsGrid for layout, Flexbox for componentsHow CSS Perspective WorksLinearly Scale font-size with CSS clamp() Based on the ViewportLearn CSS Centering JavaScript The Flavors of Object-Oriented Programming (in JavaScript)Understanding the Event Loop, Callbacks, Promises, and Async/Await in JavaScript","keywords":"articles stumble daily routine reading news blogs diving deep topic describing basics techniques day tips type ahh works uuh scatch surface time read detail stop learning pleasure so… css viewport unitsgrid layout flexbox componentshow perspective workslinearly scale font-size clamp based viewportlearn centering javascript flavors object-oriented programming javascriptunderstanding event loop callbacks promises async/await","text":"Some articles I stumble upon in my daily routine of reading news and blogs are diving very deep in a certain topic, especially if they are describing the basics of techniques I use every day. All of the following reading tips are of the type “ahh, that’s why this works like that” or “uuh, I just scatch on the surface on that”. Take your time and read the articles in detail. We all never stop learning and it’s a pleasure to do so… CSS CSS Viewport UnitsGrid for layout, Flexbox for componentsHow CSS Perspective WorksLinearly Scale font-size with CSS clamp() Based on the ViewportLearn CSS Centering JavaScript The Flavors of Object-Oriented Programming (in JavaScript)Understanding the Event Loop, Callbacks, Promises, and Async/Await in JavaScript CSS Viewport Units by Ahmad Shadeed https://ishadeed.com/article/viewport-units Ahmad is a true master of CSS and describes complex topics in an understandable way. Here he deals with the different Viewport Units: how they are calculated and how to use them properly. Grid for layout, Flexbox for components by Ahmad Shadeed https://ishadeed.com/article/grid-layout-flexbox-components Another one from Ahmad. Here he talks about the usage of Grid and/or Flexbox. Both techniques have their purpose and he shows when to use this or that. How CSS Perspective Works by Amit Sheen https://css-tricks.com/how-css-perspective-works Amit shows in this tutorial how to deal with perspective on using transform and animation in CSS. A true eye opener… Linearly Scale font-size with CSS clamp() Based on the Viewport by Pedro Rodriguez https://css-tricks.com/linearly-scale-font-size-with-css-clamp-based-on-the-viewport Few of us really deal with repsonsive typography. We fiddle arounf with line-height and font-size to achieve an B+ effect. Pedro shows how do it right with clamp() … and it is amazing. Centering in CSS by Ahmad Shadeed https://ishadeed.com/article/learn-css-centering Ahmad again (I told you, he is amazing). In this tutorial he goes through every technique to center stuff in CSS. Never again google ‘center text flexbox’… The Flavors of Object-Oriented Programming (in JavaScript) by Zell Liew https://css-tricks.com/the-flavors-of-object-oriented-programming-in-javascript There are different methods to ‘organize’ your JavaScript code. Zell shows the possibilities and pitfalls of techniques like Constructor Functions, Classes, Factory Functions and OLOO. Huge post, but couldn’t stop reading… Understanding the Event Loop, Callbacks, Promises, and Async/Await in JavaScript by Tania Rascia https://www-digitalocean-com.cdn.ampproject.org/v/s/www.digitalocean.com/community/tutorials/understanding-the-event-loop-callbacks-promises-and-async-await-in-javascript.amp?usqp=mq331AQFKAGwASA%3D&_js_v=0.1 Tanias deep knowledge of asynchronous JavaScript techniques and its basics is as long as this tutorials title and its Url. A must-read.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"Tutorial","slug":"Tutorial","permalink":"https://kiko.io/tags/Tutorial/"}]},{"title":"Device Class Detection in JavaScript","subtitle":"The unusual way by using CSS Media Queries","date":"2020-09-28","updated":"2020-09-28","path":"post/Device-Class-Detection-in-JavaScript/","permalink":"https://kiko.io/post/Device-Class-Detection-in-JavaScript/","excerpt":"In some occasions it is necessary to know which device a user is using while writing JavaScript Web Apps. Should be nothing regarding layout, because for this we have CSS Media Queries. Somewhere around 2011 W3C introduced matchMedia(), which returns a MediaQueryList object that can be used to detemnine if the document matches the media query string. The using is pretty straightforward and feels a bit like RegEx matching in JS: 1234const mediaQuery = window.matchMedia('(min-width: 1025px)')if (mediaQuery.matches) { // do something... } If you are interested in this API, you will find good introductions to the topic here, here and here (German). One point of criticism on this pure JS approach can be, that you have to maintain the breakpoints in addition to CSS … but why not use these existing breakpoints in JS?","keywords":"occasions device user writing javascript web apps layout css media queries w3c introduced matchmedia returns mediaquerylist object detemnine document matches query string pretty straightforward feels bit regex matching js 1234const mediaquery = windowmatchmedia'min-width 1025px'if mediaquerymatches { // } interested api find good introductions topic german point criticism pure approach maintain breakpoints addition … existing","text":"In some occasions it is necessary to know which device a user is using while writing JavaScript Web Apps. Should be nothing regarding layout, because for this we have CSS Media Queries. Somewhere around 2011 W3C introduced matchMedia(), which returns a MediaQueryList object that can be used to detemnine if the document matches the media query string. The using is pretty straightforward and feels a bit like RegEx matching in JS: 1234const mediaQuery = window.matchMedia('(min-width: 1025px)')if (mediaQuery.matches) { // do something... } If you are interested in this API, you will find good introductions to the topic here, here and here (German). One point of criticism on this pure JS approach can be, that you have to maintain the breakpoints in addition to CSS … but why not use these existing breakpoints in JS? If you implement a feature that is based on the different device classes, you don’t have to determine the current class with dozens of lines of JavaScript code, if you just can ask the DOM. The CSS/JS Breakpoint HackFor this approach, we take advantage of the fact, that CSS can be used to define not only styles, but also content. We always use it, when showing an icon by using a symbol font like FontAwesome: 1234my-fancy-icon::before { font-family: FontAwesome5Solid; content: "\\f186";} Mixed with a @media rule, we can “inject” the needed device value into the DOM, for example into the BODY tag, but you can take whatever you want: 12345@media (min-width: 1025px) { body:before { content: "DESKTOP"; }} Just one line more in the masses of CSS code to make a Web App responsive, but with this one you can do without many lines of JS. Now you can read out this value via JavaScript by getting the styles of the tag and get the injected content: 12var style = window.getComputedStyle(document.querySelector("body"), ":before");var breakpoint = style.getPropertyValue("content").replace(/\\"/g, ""); It is advisable to embed this request into an event listener of DOMContentLoaded, because the rule has to be set, before you can access it. See a simple working pen:","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"},{"name":"MediaQuery","slug":"MediaQuery","permalink":"https://kiko.io/tags/MediaQuery/"}]},{"title":"404 Page in Hexo for GitHub Pages","subtitle":"Provide an error page automatically when resource not found","series":"A New Blog","date":"2020-09-23","updated":"2020-09-23","path":"post/404-Page-in-Hexo-for-GitHub-Pages/","permalink":"https://kiko.io/post/404-Page-in-Hexo-for-GitHub-Pages/","excerpt":"As this blog is a static one, generated by Hexo and hostet at GitHub, the page which was shown, when a user enters an Url which points to nowhere, was the default GitHub 404 page.","keywords":"blog static generated hexo hostet github page shown user enters url points default","text":"As this blog is a static one, generated by Hexo and hostet at GitHub, the page which was shown, when a user enters an Url which points to nowhere, was the default GitHub 404 page. Not optimal and should be solved by an own Hexo page, because GitHub Pages allows you to deliver a custom 404 page by creating simply a 404.html in the root of the website. As you can create separate pages in Hexo, this is done quickly by: 1hexo new page "404" It generates a new folder named 404 in your source folder, where a index.md is placed. In this file you can enter the text as Markdown you want to show to the user, in case of a 404 error (page not found) occurs. On generating the static files by hexo generate, a subfolder 404 with a index.html will be created, which doesn’t really work with GitHub Pages, because it needs a 404.htm in the root. You can fix this, by defining the permalink in the Frontmatter of your page: 1234---title: 404permalink: /404.html--- Example … click here: https://kiko.io/no-page-here","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Error","slug":"Error","permalink":"https://kiko.io/tags/Error/"}]},{"title":"Pimping the Permalink","subtitle":"How to copy and share the permalink programatically","series":"A New Blog","date":"2020-09-20","updated":"2020-09-20","path":"post/Pimping-the-Permalink/","permalink":"https://kiko.io/post/Pimping-the-Permalink/","excerpt":"Until now I did not show the permalink under my posts in this blog, but in the past I had sometimes the need to pass one of the links and it was not very user-friendly, on desktop as well as on mobile. Not the One-Click experience I prefer. My goal was to show the permalink and, even more important, provide a simple way to copy and to share. JavaScript to the rescue…","keywords":"show permalink posts blog past pass links user-friendly desktop mobile one-click experience prefer goal important provide simple copy share javascript rescue…","text":"Until now I did not show the permalink under my posts in this blog, but in the past I had sometimes the need to pass one of the links and it was not very user-friendly, on desktop as well as on mobile. Not the One-Click experience I prefer. My goal was to show the permalink and, even more important, provide a simple way to copy and to share. JavaScript to the rescue… DisplayAs I run my blog with Hexo, I deal with EJS files. To show the permalink in my article.ejs, was quite simple. First step was to create a new partial file named permalink.ejs, to be called every time when the complete article has to be rendered: 123<% if (!index){ %> <%- partial('post/permalink', { class_name: 'article-permalink' }) %><% } %> The partial file looked like this in this step: 123<div class="<%= class_name %>""> <a id="article-permalink" href="<%- post.permalink %>"><%- post.permalink %></a></div> CopyAs I read a little bit about the possibilities to copy text into the clipboard via JavaScript on MDN, it became obvious that a link is not the best solution, because using the exeCommand needs to have something selected and this is difficult on anchors. Then … do it with an input: 123456789101112131415161718192021222324252627<div class="<%= class_name %>""> <input id="article-permalink" value="<%- post.permalink %>" /> <a id="action-copy" class="article-action" href="javascript:copyPermalink();"></a></div><script> var copyText = document.querySelector("#article-permalink"); //Disable Input by default copyText.disabled = true; function copyPermalink() { //Enable Input copyText.disabled = false; //Select permalink text copyText.select(); //Copy to clipboard document.execCommand("copy"); //Remove selection again copyText.blur(); //Disable Input again copyText.disabled = true; }</script> Nice, but a user feedback, that the text has been copied to the clipboard, was advisable, because nothing is more annoying, when you click somewhere and nothing seems to happen. As I hate default browser confirmations and other distracting messaging methods, I wanted to use the input itself, by fading out the link text, replace it with a message and fade in the text again: I extended my animation.styl (Hexo works with Stylus) with two keyframe animations … one for fading in, one for fading out… 12345678910111213141516171819202122232425@keyframes fadeIn { 0% { opacity:0; } 100% { opacity:1; }}.fade-in-500 animation: fadeIn ease 0.5s;.fade-in-1000 animation: fadeIn ease 1s;@keyframes fadeOut { 0% { opacity:1; } 100% { opacity:0; }}.fade-out-500 animation: fadeOut ease 0.5s;.fade-out-1000 animation: fadeOut ease 1s; … and wrote a setTimeout cascade to achive the effect: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152<div class="<%= class_name %>""> <input id="article-permalink" value="<%- post.permalink %>" /> <a class="article-action action-copy" href="javascript:copyPermalink();"></a></div><script> var copyText = document.querySelector("#article-permalink"); copyText.disabled = true; function copyPermalink() { copyText.disabled = false; copyText.select(); document.execCommand("copy"); copyText.blur(); copyText.disabled = true; //Store original text var permalink = copyText.value; //Start fading out copyText.classList.add("fade-out-500"); //Wait until animation is done setTimeout(function(){ //Set message, remove fadout class and add start fading in copyText.value = "copied to clipboard"; copyText.classList.remove("fade-out-500"); copyText.classList.add("fade-in-1000"); //Wait 2 seconds to show the message setTimeout(function() { //Start to fade out message copyText.classList.add("fade-out-500"); //Wait until animation is done setTimeout(function() { //Set original text again and remove fadout class copyText.value = permalink; copyText.classList.remove("fade-out-500"); //Wait until animation is done setTimeout(function() { //Remove fadeout class copyText.classList.remove("fade-in-1000"); }, 500); }, 500); }, 2000); }, 500); }</script> ShareThe second permalink feature was a little bit trickier, because I didn’t want to use one of the sharing libraries out there, whose business model is based on my readers data (always keep conservative on implementing third party stuff, because you never know what they are doing with the data). But a couple of months ago I read about a new native browser API for WebApps on the rise: Web Share API. Since 2019 W3C is working on this API, for sharing text, links and other content to an arbitrary destination of the user’s choice. On 27 August 2020 the published a Working Draft and on 16 September 2020 the latest Editors Draft. Brand new stuff. The browser support is not the best yet, but it will be getting better in the near feature, especially as Edge Chrome is one of the early adopters. web.dev lists important requirements on using this new feature in JavaScript: It can only be used on a site that supports HTTPS It must be invoked in response to a user action such as a click But it can share URL’s, text and even files! A raw implementation can be: 12345678910if (navigator.share === undefined) { navigator.share({ title: 'My Post', url: 'https://my-domain.com/my-url', }) .then(() => console.log('Successful share')) .catch((error) => console.log('Error sharing', error));} else { // fallback} I refrain to implement a fallback, rather I would like to show the appropriate button only to those users, whose browser supports it: 12345678910111213141516171819202122<div class="<%= class_name %>""> <input id="article-permalink" value="<%- post.permalink %>" data-id="<%= post._id %>" /> <a id="action-copy" class="article-action" href="javascript:copyPermalink();"></a> <a id="action-share" class="article-action" href="javascript:sharePermalink();"></a></div><script> function copyPermalink() { -- SEE ABOVE } if (navigator.share === undefined) { var shareLink = document.querySelector("#action-share"); shareLink.style.display = "none"; } function sharePermalink() { navigator.share({ title: "<%- post.title %>", url: "<%- post.permalink %>", }) }</script> More Info w3c.github.io: W3C Web Share Testheise Developer: Features von übermorgen: Die Web Share API und die Web Share Target API (German)CSS-Tricks: How to Use the Web Share API","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Stylus","slug":"Stylus","permalink":"https://kiko.io/tags/Stylus/"},{"name":"Share","slug":"Share","permalink":"https://kiko.io/tags/Share/"}]},{"title":"Discoveries #2","subtitle":null,"series":"Discoveries","date":"2020-09-07","updated":"2020-09-07","path":"post/Discoveries-2/","permalink":"https://kiko.io/post/Discoveries-2/","excerpt":"New month, new discoveries. We will deal with key bindings, downloads on the fly, a lot of animations and contrasting images. Have fun, trying out these stunning solutions. tinykeys - Modern library for keybindingsCreating files in JavaScript in your browserCSS Animated Google FontsSkeleton Screen CSSMore Control Over CSS Borders With background-imageA CSS-only, animated, wrapping underlineNailing the Perfect Contrast Between Light Text and a Background ImageContrast.js","keywords":"month discoveries deal key bindings downloads fly lot animations contrasting images fun stunning solutions tinykeys - modern library keybindingscreating files javascript browsercss animated google fontsskeleton screen cssmore control css borders background-imagea css-only wrapping underlinenailing perfect contrast light text background imagecontrastjs","text":"New month, new discoveries. We will deal with key bindings, downloads on the fly, a lot of animations and contrasting images. Have fun, trying out these stunning solutions. tinykeys - Modern library for keybindingsCreating files in JavaScript in your browserCSS Animated Google FontsSkeleton Screen CSSMore Control Over CSS Borders With background-imageA CSS-only, animated, wrapping underlineNailing the Perfect Contrast Between Light Text and a Background ImageContrast.js tinykeys - Modern library for keybindings by Jamie Kyle https://jamiebuilds.github.io/tinykeys Very easy to use key binding library for JavaScript. Supports key sequences and modifier keys. Creating files in JavaScript in your browser by Kilian Valkhof https://kilianvalkhof.com/2020/javascript/creating-files-in-javascript-in-your-browser Kilian shows how to prepare data in JavaScript and offer them to download on the fly, without the use of storing a file. CSS Animated Google Fonts by Jhey Tompkins https://dev.to/jh3y/animated-google-fonts-193d As Google Fonts now supports variable fonts, Jhey shows a solution how to create neat font animations with them. Skeleton Screen CSS by Dmitriy Kuznetsov https://github.com/nullilac/skeleton-screen-css When loading data on demand, it is sometimes advisable to show placeholders, where the data will be filled in. Dimitriy has founded a CSS framework for these skeletons. More Control Over CSS Borders With background-image by Chris Coyier https://css-tricks.com/more-control-over-css-borders-with-background-image Borders are used to seperate things in a layout, but the build-in possibilities of CSS are restricted. Chris found a way by pimping borders up, using background images. A CSS-only, animated, wrapping underline by Nicky Meuleman https://nickymeuleman.netlify.app/blog/css-animated-wrapping-underline As Chris did for the borders, Nick’s doing on underlined links. An end to boring rigid unterlines, let’s animate them. Nailing the Perfect Contrast Between Light Text and a Background Image by Yaphi Berhanu https://css-tricks.com/nailing-the-perfect-contrast-between-light-text-and-a-background-image Showing text on background images can be challenging due to contrast and readability. Yaphi has developed a solution to find always the right transparent overlay to show the most of the picture, but keep the text readable. Stunning… Contrast.js by Misha Petrov https://github.com/MishaPetrov/Contrast.js Misha addresses the same problem as Yaphi, showing text on background images, but goes a different way with his library, which is trying to find the best constrasting text color, even if the page is responsive.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Add website to Trello card the better way","subtitle":"Avoid default share, use the Trello bookmarklet","date":"2020-09-07","updated":"2020-09-07","path":"post/Add-website-to-Trello-card-the-better-way/","permalink":"https://kiko.io/post/Add-website-to-Trello-card-the-better-way/","excerpt":"I was looking for a new way to store interesting website articles for reading later, as Pocket, my favourite tool until here, gets worse and worse. As I am a big Trello fan, I wanted to give it a chance to be Pockets successor on my smartphone, where I’m reading mostly. On installing the Trello Android app, you will find a new SHARE target Add new Trello card, which is comfortable to use: (Sry, for the German screenshots ;) The result, website’s title and Url set, is nice at best: … but Trello has a Bookmarklet, which does the job much better.","keywords":"store interesting website articles reading pocket favourite tool worse big trello fan wanted give chance pockets successor smartphone im installing android app find share target add card comfortable sry german screenshots result websites title url set nice … bookmarklet job","text":"I was looking for a new way to store interesting website articles for reading later, as Pocket, my favourite tool until here, gets worse and worse. As I am a big Trello fan, I wanted to give it a chance to be Pockets successor on my smartphone, where I’m reading mostly. On installing the Trello Android app, you will find a new SHARE target Add new Trello card, which is comfortable to use: (Sry, for the German screenshots ;) The result, website’s title and Url set, is nice at best: … but Trello has a Bookmarklet, which does the job much better. The following approach works best in the Google Chrome browser. First, a Bookmarklet is a small piece of JavaScript, which is stored as a bookmark in your browser. As you can’t actually create such a Bookmarklet in your Android Chrome, you have to create it in your desktop Chrome and switch on the bookmark sync of chrome. You should right away choose a short, concise name for the bookmark, so you find it easier in Android Chrome afterwards. I called it 2TrelloCard, because few websites start with an number. After Chrome’s sync is done, go to any website do you want to store as a Trello card. Now enter the Url box and type the name of the bookmarklet and select it. Instead of requesting a different page, Chrome executes the JavaScript of the Bookmarklet against the currently open website. This script shows a Trello dialog, where you can choose, which board and list the new card should be created on. This card creation method not only sets the title of the card, but fills the description with the meta description of the page, adds the first found meta image as cover and adds the Url as an attachment:","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Trello","slug":"Trello","permalink":"https://kiko.io/tags/Trello/"},{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"}]},{"title":"Horizontal navigation menu above an image","subtitle":"How to deal with coverage, readability and scrollbars","series":"A New Blog","date":"2020-07-20","updated":"2020-07-20","path":"post/Horizontal-navigation-menu-above-an-image/","permalink":"https://kiko.io/post/Horizontal-navigation-menu-above-an-image/","excerpt":"I changed the main menu of my blog, because I wanted to get rid of the hamburger menu on the upper left, which was shown only for smartphones, but was not really reachable conveniently. Beside that it made no sense to have different navigations for different devices. My choice was to implement a horizontal scrolling menu, which can grow over the time, without any need of customizing. As I have quite big header images and I wanted to place the new navigation in a more accessible zone, I decided to place it at the bottom, but above the header image.","keywords":"changed main menu blog wanted rid hamburger upper left shown smartphones reachable conveniently made sense navigations devices choice implement horizontal scrolling grow time customizing big header images place navigation accessible zone decided bottom image","text":"I changed the main menu of my blog, because I wanted to get rid of the hamburger menu on the upper left, which was shown only for smartphones, but was not really reachable conveniently. Beside that it made no sense to have different navigations for different devices. My choice was to implement a horizontal scrolling menu, which can grow over the time, without any need of customizing. As I have quite big header images and I wanted to place the new navigation in a more accessible zone, I decided to place it at the bottom, but above the header image. Problem was, not to cover a big part of the image with a full-colored or even semitransparent bar, by using a RGBA background color. I wanted it more translucent, but with enough contrast on bright images for the menu items to read. The recently introduced W3C feature backdrop-filter was just the right thing for that. It is supported by most modern browsers, but it has to have a backup strategy for the rest of the bunch. The HTML is simple: 1234567891011121314151617<nav id="header-nav" role="navigation"> <ul class="menu"> <li class="menu-item"> <a href="/first" title="First"> <span>First Item</span> </a> </li> <li class="menu-item"> <a href="/second" title="Second"> <span>Second Item</span> </a> </li> </ul></nav> And here’s the Stylus code for my approach: 123456789101112131415161718192021222324252627282930313233343536373839404142#header-nav position: absolute bottom: 0 width: 100% height: auto box-sizing: content-box overflow-x: scroll overflow-y: hidden // BACKDROP-FILTER backdrop-filter: blur(5px) brightness(90%) @supports not (backdrop-filter: none) background: rgba(0,0,0,0.25) // SCROLLBAR &::-webkit-scrollbar display: none @supports not (webkit-scrollbar) scrollbar-width: none .menu display: flex list-style: none margin: 0 padding: 0 .menu-item flex-basis: 80px flex-shrink: 0 flex-grow: 1 max-width: 100px margin: 0 2px text-overflow: ellipsis; a display: inline-block width: 100% padding: 10px 0 color: #ffffff font-weight: bold text-decoration: none text-align: center The navigation box is absolute positioned on the image, is as wide as the screen and scrolls exclusively horizontal. The items are a unordered list, with default width and arranged by flex. In case a browser doesn’t understand backdrop-filter, the navigation bar is shown with a classic alpha channel opacity. When having a horizontal scroll feature, the scrollbar shown by the browser is beyond beautiful. To prevent this, I used the CSS pseudo element ::-webkit-scrollbar, which is supported by WebKit and Blink bowsers, with a fallback for all other browsers. Both strategies allows to be still able to scroll. If you want to have a scrollbar, but not the built-in, I can only recommend to read something about styling scrollbars, like here and here.","categories":[{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Stylus","slug":"Stylus","permalink":"https://kiko.io/tags/Stylus/"}]},{"title":"Change CSS class when element scrolls into viewport","subtitle":null,"series":"A New Blog","date":"2020-07-13","updated":"2020-07-13","path":"post/Change-CSS-class-when-element-scrolls-into-viewport/","permalink":"https://kiko.io/post/Change-CSS-class-when-element-scrolls-into-viewport/","excerpt":"I had a neat visual gimmick on the start page of this blog, that the gray-scaled header image of a post in the list scaled up to 100% and became colored, when the user hovered over it: 123456789101112131415.article-inner .article-photo { height: 150px; width: 100%; object-fit: cover; transform: scale(1); transform-style: preserve-3d; transition: all ease-out 0.6s; opacity: 0.3; filter: grayscale(1) contrast(0.5);}.article-inner:hover .article-photo { transform: scale(1.1); opacity: 1; filter: grayscale(0) contrast(1);} Nice, but a little bit useless on smartphones or tablets, where HOVER doesn’t really work.","keywords":"neat visual gimmick start page blog gray-scaled header image post list scaled 100% colored user hovered 123456789101112131415article-inner article-photo { height 150px width object-fit cover transform scale1 transform-style preserve-3d transition ease-out 06s opacity filter grayscale1 contrast05}article-innerhover scale11 grayscale0 contrast1} nice bit useless smartphones tablets hover doesnt work","text":"I had a neat visual gimmick on the start page of this blog, that the gray-scaled header image of a post in the list scaled up to 100% and became colored, when the user hovered over it: 123456789101112131415.article-inner .article-photo { height: 150px; width: 100%; object-fit: cover; transform: scale(1); transform-style: preserve-3d; transition: all ease-out 0.6s; opacity: 0.3; filter: grayscale(1) contrast(0.5);}.article-inner:hover .article-photo { transform: scale(1.1); opacity: 1; filter: grayscale(0) contrast(1);} Nice, but a little bit useless on smartphones or tablets, where HOVER doesn’t really work. A better idea was to transform the header image automatically, when it becomes visible to the user. So I changed the HOVER selector into a class… 12345.article-photo.in-view { transform: scale(1.1); opacity: 1; filter: grayscale(0) contrast(1);} … and wrote a little JS function to determine the point, where the images is fully visible in the viewport: 1234567function isVisibleInViewPort(e) { var viewTop = $(window).scrollTop(); var viewBottom = viewTop + $(window).height(); var eTop = $(e).offset().top; var eBottom = eTop + $(e).height(); return ((eBottom <= viewBottom) && (eTop >= viewTop));} This function I had to bind to the windows scroll event to all header images only: 123456789$(window).on('scroll', function() { $(".article-photo").each(function() { if (isVisibleInViewPort($(this))) { $(this).addClass("in-view"); } else { $(this).removeClass("in-view"); } });});","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"jQuery","slug":"jQuery","permalink":"https://kiko.io/tags/jQuery/"}]},{"title":"Discoveries #1","subtitle":null,"series":"Discoveries","date":"2020-07-12","updated":"2020-07-12","path":"post/Discoveries-1/","permalink":"https://kiko.io/post/Discoveries-1/","excerpt":"Due to my daily routine, I’m reading a lot of articles on the web regarding software development. The most interesting stuff ends up on my Pocket list, which grows from day to day. Hard to find the pearls, when I need them. This recurring posts will throw a stroke of light on them. They are maybe not the newest finds, not the fanciest ones, but remarkable for me and maybe for you also. Pure CSS halftone portrait from .jpg sourceScrollTrigger - Highlight TextTiny long-press event handlerShow More/Less3D banners with ScrollTriggerImage Compare ViewerAdd Read or Scroll Progress Bar To A Website To Indicate Read ProgressHow to Get a Progressive Web App into the Google Play Store","keywords":"due daily routine im reading lot articles web software development interesting stuff ends pocket list grows day hard find pearls recurring posts throw stroke light newest finds fanciest remarkable pure css halftone portrait jpg sourcescrolltrigger - highlight texttiny long-press event handlershow more/less3d banners scrolltriggerimage compare vieweradd read scroll progress bar website progresshow progressive app google play store","text":"Due to my daily routine, I’m reading a lot of articles on the web regarding software development. The most interesting stuff ends up on my Pocket list, which grows from day to day. Hard to find the pearls, when I need them. This recurring posts will throw a stroke of light on them. They are maybe not the newest finds, not the fanciest ones, but remarkable for me and maybe for you also. Pure CSS halftone portrait from .jpg sourceScrollTrigger - Highlight TextTiny long-press event handlerShow More/Less3D banners with ScrollTriggerImage Compare ViewerAdd Read or Scroll Progress Bar To A Website To Indicate Read ProgressHow to Get a Progressive Web App into the Google Play Store Pure CSS halftone portrait from .jpg source by Ana Tudor https://codepen.io/thebabydino/pen/LYGGwrm Ana, author at CSS Tricks, shows a CSS-only technique to convert an image into a halftone one. ScrollTrigger - Highlight Text by Ryan Mulligan https://codepen.io/hexagoncircle/details/gOPMwvd We all highlight important text passages for our readers. Ryan does the in an unusual, butt cool way by using GSAP ScrollTrigger. Tiny long-press event handler by MudOnTire https://github.com/MudOnTire/web-long-press Vanilla JS multi-instance handling of long press event the easy way. Show More/Less by Grzegorz Tomicki https://github.com/tomik23/show-more Grzegorz’s little JS helper to cut texts, lists and even tables and show a MORE link. 3D banners with ScrollTrigger by supamike https://codepen.io/supamike/full/KKVqXmR Awesome 3D effect on scrolling made with ScrollTrigger. Image Compare Viewer by Kyle Wetton https://image-compare-viewer.netlify.app/ Comparison slider in Vanilla JS to compare BEFORE and AFTER images, which works responsively on every device. Add Read or Scroll Progress Bar To A Website To Indicate Read Progress by Jun711 https://jun711.github.io/web/add-scroll-progress-bar-to-a-website-to-indicate-read-progress/ A classic, simply explained… Here another approach: CSS Tricks: Reading Position Indicator How to Get a Progressive Web App into the Google Play Store by Mateusz Rybczonek https://css-tricks.com/how-to-get-a-progressive-web-app-into-the-google-play-store/ Mateusz describes very detailed how offer your PWA as an App via Google Play Store.","categories":[{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"}],"tags":[{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"}]},{"title":"Dopamine: How Software should be...","subtitle":"A great media player for Windows 10","series":"Great Finds","date":"2020-07-10","updated":"2020-07-10","path":"post/Dopamine-How-Software-should-be/","permalink":"https://kiko.io/post/Dopamine-How-Software-should-be/","excerpt":"Not very often, when I’m looking for a new tool to replace some annoying or outdated piece of software, I have to blog about it … but from time to time, I’m discovering pearls, worth to lose a word about. The Windows 10 built-in media player Groove is (to be kind) … nice, but it is more or less a leftover from Microsoft’s attempt to create a competitor to iTunes, years ago. The crippeled UI is not the most modern and I was more than once annoyed about its usability. Doing a research for a good alternative, you stumble always over the usual suspects: MediaMonkey, Foobar2000, Winamp, VLC or even Media Player Classic!? Not modern enough, not user friendly enough, not lean enough. I really don’t remember where, but there was a screenshot of a player, which seems to be the complete opposite of the others: Dopamine from Digimezzo, a project by the Belgian developer Raphaël Godart…","keywords":"im tool replace annoying outdated piece software blog … time discovering pearls worth lose word windows built-in media player groove kind nice leftover microsofts attempt create competitor itunes years ago crippeled ui modern annoyed usability research good alternative stumble usual suspects mediamonkey foobar2000 winamp vlc classic user friendly lean dont remember screenshot complete opposite dopamine digimezzo project belgian developer raphaël godart…","text":"Not very often, when I’m looking for a new tool to replace some annoying or outdated piece of software, I have to blog about it … but from time to time, I’m discovering pearls, worth to lose a word about. The Windows 10 built-in media player Groove is (to be kind) … nice, but it is more or less a leftover from Microsoft’s attempt to create a competitor to iTunes, years ago. The crippeled UI is not the most modern and I was more than once annoyed about its usability. Doing a research for a good alternative, you stumble always over the usual suspects: MediaMonkey, Foobar2000, Winamp, VLC or even Media Player Classic!? Not modern enough, not user friendly enough, not lean enough. I really don’t remember where, but there was a screenshot of a player, which seems to be the complete opposite of the others: Dopamine from Digimezzo, a project by the Belgian developer Raphaël Godart… But that wasn’t the best, especially for me. Dopamine is written in C# as a WPF application and it is OpenSource, hosted on GitHub. The software is so wonderful lean and its integrating in Windows 10 like a charm. It has several categories to find the right music to play, a context-sensitive search, a folder view, is able to import and manage playlists, a light and dark mode and translations into currently 28 languages. It can update your collection automatically from several folders, has two player modes and is incredibly fast. The keep long story short … I fell in love on Dopamine‘s simple beauty and it is now my favourite player on Windows 10! Thanks Raphaël…","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Audio","slug":"Audio","permalink":"https://kiko.io/tags/Audio/"}]},{"title":"Using GitHub as Commenting Platform","subtitle":"Integrate Utterances' GitHub Issue Commenting to Hexo","series":"A New Blog","date":"2020-07-05","updated":"2020-07-05","path":"post/Using-GitHub-as-Commenting-Platform/","permalink":"https://kiko.io/post/Using-GitHub-as-Commenting-Platform/","excerpt":"If you run a blog, it is always advisable to integrate a commenting system, in order to get feedback on your posts from your readers. So did I, when I start this blog and I decided to use the Disqus platform, as it was very easy to integrate … but I always had a bad feeling about a third-party platform collecting data from my readers. Disqus is probably not without reason under criticism by many people in the community. As I host this blog at GitHub (see A New Blog (Part One): VS Code, Hexo and GitHub Pages) I was looking for a solution to host the comments also on my prefered platform. There were some solutions, which uses GitHub Issues for this and wanted to implement something like that someday.","keywords":"run blog advisable integrate commenting system order feedback posts readers start decided disqus platform easy … bad feeling third-party collecting data reason criticism people community host github part code hexo pages solution comments prefered solutions issues wanted implement someday","text":"If you run a blog, it is always advisable to integrate a commenting system, in order to get feedback on your posts from your readers. So did I, when I start this blog and I decided to use the Disqus platform, as it was very easy to integrate … but I always had a bad feeling about a third-party platform collecting data from my readers. Disqus is probably not without reason under criticism by many people in the community. As I host this blog at GitHub (see A New Blog (Part One): VS Code, Hexo and GitHub Pages) I was looking for a solution to host the comments also on my prefered platform. There were some solutions, which uses GitHub Issues for this and wanted to implement something like that someday. As I read a post from on Thomas Lavesques’ blog, to solve another problem, his commenting section came to my attention: utteranc.es … exactly the solution I needed! Thanx guys… On their website is a small configurator for a script to implement in each post, which needs only few information: Name of the Repo How the mapping of the post to the Issues should work Name of the Theme, in order to fit to the colors of the blog The script had to be included to my Hexo article.js: 123456789<% if (!index && post.comments){ %> <script src="https://utteranc.es/client.js" repo="kristofzerbe/kiko.io" issue-term="pathname" theme="github-light" crossorigin="anonymous" async> </script><% } %> That’s pretty much it. On entering the first comment, Utterances told me to install the needed GitHub App to my repo, in order to make it work … and done. The result you see below … UPDATE…The utterances script tag has the attribute theme, to tell utterances which style should be delivered. There are several themes available, but if users are able to switch between light or dark mode on the page (see Hexo and the Dark Mode), the comment block should change to an suitable theme also. On order to respond on a mode change, it is necessary to write a more dynamic script loading. First we define a function in a global script file to load the utterances script via JS: 1234567891011121314151617181920function insertUtterancesCommentBlock() { var commentTheme = "github-light"; if(localStorage.getItem("theme") === "dark"){ commentTheme = "github-dark"; } const scriptId = "comment-theme-script"; const existingScript = document.getElementById(scriptId); if (!existingScript) { const commentScript = document.createElement("script"); commentScript.id = scriptId; commentScript.src = "https://utteranc.es/client.js"; commentScript.setAttribute("repo", "kristofzerbe/kiko.io"); commentScript.setAttribute("issue-term", "pathname"); commentScript.setAttribute("theme", commentTheme); commentScript.setAttribute("crossorigin", "anonymous"); const placeholder = document.getElementById("comment-placeholder"); placeholder.innerHTML = ""; placeholder.appendChild(commentScript); }} Then we change the placement in the EJS file, by defining a placeholder and ensuring that the script above is loaded, before we call it: 123456<div id="comment-placeholder"></div><script> window.addEventListener('load', function () { insertUtterancesCommentBlock(); })</script> On my blog, everytime the user switches between light/dark mode the body tag will be decorated with the data tag data-theme and the value of the mode. To keep the loading of the utterances script independent from this functionality, we just have to listen to this change via MutationObserver: 12345678910//observe theme change, to adjust comment block themevar target = document.documentElement, observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.attributionName === "data-theme" ); insertUtterancesCommentBlock(); }); }), config = { attributes: true };observer.observe(target, config);","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"Meaningful automatic versioning with T4","subtitle":"How to implement versioning in C# projects the better way","date":"2020-06-27","updated":"2020-06-27","path":"post/Meaningful-automatic-versioning-with-T4/","permalink":"https://kiko.io/post/Meaningful-automatic-versioning-with-T4/","excerpt":"Every developer has to have an idea of versioning his products. If you work with Visual Studio you have the Assembly Information in the project properties dialog, to enter it manually everytime you want to release a new version: The four fields are: MAJOR, MINOR, BUILD, REVISION. But seriously … who does that? I guess 99% of all C# developers are entering the AssemblyInfo.cs and enter the famous 2 asterisks into the version declaration of BUILD and REVISION, to let Visual Studio do the incrementation job: 12[assembly: AssemblyVersion("1.0.*.*")][assembly: AssemblyFileVersion("1.0.*.*")] But this is not the end of the possibilities … Let’s do it more meaningful, with some goodies and still automatic…","keywords":"developer idea versioning products work visual studio assembly information project properties dialog enter manually everytime release version fields major minor build revision … guess 99% c# developers entering assemblyinfocs famous asterisks declaration incrementation job 12[assembly assemblyversion"10**"][assembly assemblyfileversion"10**"] end possibilities lets meaningful goodies automatic…","text":"Every developer has to have an idea of versioning his products. If you work with Visual Studio you have the Assembly Information in the project properties dialog, to enter it manually everytime you want to release a new version: The four fields are: MAJOR, MINOR, BUILD, REVISION. But seriously … who does that? I guess 99% of all C# developers are entering the AssemblyInfo.cs and enter the famous 2 asterisks into the version declaration of BUILD and REVISION, to let Visual Studio do the incrementation job: 12[assembly: AssemblyVersion("1.0.*.*")][assembly: AssemblyFileVersion("1.0.*.*")] But this is not the end of the possibilities … Let’s do it more meaningful, with some goodies and still automatic… More informative versioningA build with an increased MAJOR version number means, that there are significant changes in the product, even breaking changes. This always should be set manually. Also the MINOR. It stands for significant functional extensions of the product. How does Visual Studio calculate BUILD and REVISION? When specifying a version, you have to at least specify major. If you specify major and minor, you can specify an asterisk for build. This will cause build to be equal to the number of days since January 1, 2000 local time, and for revision to be equal to the number of seconds since midnight local time, divided by 2. But, the BUILD number should explain, how often a software with a particular MAJOR.MINOR has been build, due to minor changes and bug fixes. The “Asterisk” REVISION number is a little weird, but at least with the BUILD number unique. But it says nothing. Better to pick up the idea of a date calculated, unique number, but not an arbitrary date … let’s take the date the project has started. For example: 1.2.16.158 … reads version 1.2 with 16 builds on the 158’th day after the project has started. Start with T4T4 (Text Template Transformation Toolkit) is a templating system in Visual Studio for generating text files during design time. It is very suitable to even generate code. Read about it here and here. A Text Template (.tt) has Directives (how the template is processed), Text blocks (text copied to the output) and Control blocks (program code). For our versioning template, we start with this in a new file named AssemblyVersion.tt: Directives: 12<#@ template hostspecific="true" language="C#" #><#@ output extension=".cs" #> Control block: 123456<# int major = 1; int minor = 0; int build = 0; int revision = 0;#> Text block: 1234567// This code was generated by a tool. Any changes made manually will be lost// the next time this code is regenerated.using System.Reflection;[assembly: AssemblyVersion("<#= $"{major}.{minor}.{build}.{revision}" #>")][assembly: AssemblyFileVersion("<#= $"{major}.{minor}.{build}.{revision}" #>")] On saving the TT file, a new CS file with the same name will be created automatically and you got an error like this: A new place for version infoTh error occurs, because we have now two AssemblyVersion and AssemblyFileVersion attributes in our project. We need to comment out the original in Properties\\AssemblyInfo.cs: Structural ConsiderationsIt makes sense to store all needed files for the new versioning system in a new root folder of the project, named AssemblyVersion, starting with the AssemblyVersion.tt, because there will be more files later on. New app information fileAs we replaced the original version attributes in the project with those from our generated AssemblyVersion.cs, we cannot control the MAJOR and MINOR version number via the project property dialog any longer. We need a new approach on that, which can be edited easily and processed automatically. AssemblyVersion.json1234567891011121314151617{ "initialDate": "2019-09-29", "versions": [ { "major": 1, "minor": 1, "releaseDate": "", "remarks": "Some cool new features; New versioning system" }, { "major": 1, "minor": 0, "releaseDate": "2019-10-01", "remarks": "Initial Release" } ]} This new JSON file has two main items: initialDate - the date the project has started, to calculate the REVISION later on versions - a list with all different MAJOR/MINOR versions we have done so far, with at least one without a release date … the one with the highest major and minor. The remarks attribute of a list item holds some information about the changes in a new version. Together with releaseDate, useful for a possible release history, shown in the product itself. Library references in T4T4 runs in its own app domain, therefore it can use built-in libraries as System.IO, but not third-party libraries like Newtonsoft.JSON. We could reference those libraries from the projects package folder via the absolute path (if we use it in our product), but when we are running a NuGet update, the reference will break. It is advisable to store such libraries directly in a fixed folder, like AssemblyVersion\\Libraries. They won’t have any impact to our product, because the are only used while design time. The MAJOR and MINORTo process the new AssemblyVersion.json in the template, we need some new directives for referencing the needed libraries and the import of the appropriate namepaces: 123456<#@ assembly name="System.Core" #><#@ assembly name="$(SolutionDir)\\AssemblyVersion\\Libraries\\Newtonsoft.Json.dll" #><#@ import namespace="System.IO" #><#@ import namespace="System.Linq" #><#@ import namespace="Newtonsoft.Json" #> Via the use of the T4 variable $(SolutionDir), we can point to our copy of Newtonsoft JSON. Now we can read and convert the JSON into an anonymous object and get the highest values of MAJOR and MINOR: 12345678910111213141516171819202122232425262728<# string avPath = this.Host.ResolvePath("AssemblyVersion.json"); string avJson = File.ReadAllText(avPath); var avDefinition = new { initialDate = "", versions = new [] { new { major = 0, minor = 0, releaseDate = "", remarks = "" } } }; var avObject = JsonConvert.DeserializeAnonymousType(avJson, avDefinition); //Get highest Major/Minor from versions list var maxVersion = avObject.versions .OrderByDescending(i => i.major) .ThenByDescending(j => j.minor) .First(); //Set MAJOR int major = maxVersion.major; //Set MINOR int minor = maxVersion.minor;#> The BuildLogIn order to get the version number for BUILD, we need a method to count and store every build that has been run, separated by the MAJOR/MINOR versions. This is a job for a Post-build event, which can be configured in the project properties dialog. The event uses shell commands as they are used on the command line. What the commands should do: Write a new line with the current date and time in a log file, named after the MAJOR/MINOR version and stored in the folder AssemblyVersion\\BuildLogs. Extending build event macrosShell commands for build events are supporting built-in variables, so called ‘macros’, like $(ProjectDir) (which returns the project directory path), but there is no such macro for the current version number. We have to introduce it via extending the project with a new build target. Unload the project in Visual Studio for editing the CSPROJ (or VBPROJ) file of your product manually and write the following definition just before the end-tag: 123456789101112131415<PropertyGroup> <PostBuildEventDependsOn> $(PostBuildEventDependsOn); PostBuildMacros; </PostBuildEventDependsOn></PropertyGroup><Target Name="PostBuildMacros"> <GetAssemblyIdentity AssemblyFiles="$(TargetPath)"> <Output TaskParameter="Assemblies" ItemName="Targets" /> </GetAssemblyIdentity> <ItemGroup> <VersionNumber Include="@(Targets->'%(Version)')" /> </ItemGroup></Target> After reloading the project in Visual Studio, we can use @(VersionNumber) in our commands. CreateBuildLog.batThe event build editor is not very comfortable, so we create the batch file CreateBuildLog.bat in our AssemblyVersion folder and use this as the post build event command. The BuildLog folder must exist, before running the following command the first time! 123456789101112131415161718192021222324252627@echo offREM --Get parametersset PROJECT_DIR=%1set VERSION_NUMBER=%2REM --Set what to logset LOG_LINE=%DATE% %TIME%REM --Inform the userset MSG=CreateBuildLog '%LOG_LINE%' for version %VERSION_NUMBER%echo %MSG%REM --Get version partsFOR /f "tokens=1,2,3,4 delims=." %%a IN ("%VERSION_NUMBER%") do ( set MAJOR=%%a set MINOR=%%b set BUILD=%%c set REVISION=%%d)REM --Define BuildLog file and folder set BUILDLOG_FILE=%MAJOR%.%MINOR%.logset BUILDLOG_FOLDER=%PROJECT_DIR%\\AssemblyVersion\\BuildLogsREM --Write current date and time as new line in the fileecho %LOG_LINE% >> %BUILDLOG_FOLDER%\\%BUILDLOG_FILE%" 1"$(ProjectDir)\\AssemblyVersion\\CreateBuildLog.bat" "$(ProjectDir)" @(VersionNumber) The BUILDAs we have now the BuildLogs, we can use them in the template: 123456789101112131415161718192021<# ... //Get BuildLog of max version string buildlogFolder = this.Host.ResolvePath("BuildLogs"); string buildLog = buildlogFolder + "\\\\" + maxVersion.major + "." + maxVersion.minor + ".log"; //Get number of lines from BuildLog or create a new log (!) var buildCount = 1; if (File.Exists(buildLog)) { buildCount = File.ReadLines(buildLog).Count() + 1; } else { File.Create(buildLog).Dispose(); } //Set BUILD int build = buildCount;#> Very important is to create the log file, if it doesn’t exists! Otherwise the build will always fail, because the version attributes can’t be created. The REVISIONAt least we have to set the REVISION number, by calculating the difference between the current date and the initialDate, which we have previously read from the AssemblyVersion.json: 1234567<# ... //Set REVISION var dateCreated = DateTime.Parse(avObject.initialDate); int revision = (DateTime.Now.Date - dateCreated.Date).Days;#> Transforming T4 template on buildThe last hurdle is to run the text transformation every time you build your product. Until now it runs only on saving the AssemblyVersion.tt. A great helper on that was Thomas Levesque’s post “Transform T4 templates as part of the build, and pass variables from the project”, where he describes every difficulty to reach this goal. To make it short: We have to edit the CSPROJ file again, to introduce TextTemplating to MSBuild. First we need following near the beginning of the projects XML: 1234567891011<PropertyGroup> <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''"> 16.0 </VisualStudioVersion> <VSToolsPath Condition="'$(VSToolsPath)' == ''"> $(MSBuildExtensionsPath32)\\Microsoft\\VisualStudio\\v$(VisualStudioVersion) </VSToolsPath> <TransformOnBuild>true</TransformOnBuild> <OverwriteReadOnlyOutputFiles>true</OverwriteReadOnlyOutputFiles> <TransformOutOfDateOnly>false</TransformOutOfDateOnly></PropertyGroup> Secondly add the IMPORT of the TextTemplating target AFTER the CSharp target: 123<Import Project="$(MSBuildToolsPath)\\Microsoft.CSharp.targets" />...<Import Project="$(VSToolsPath)\\TextTemplating\\Microsoft.TextTemplating.targets" /> If you build your product now, a new build log is created and the version numbers BUILD and REVISION are automatically increased. See it in actionThe project where I implemented this versioning first is HexoCommander. Feel free to download the code and see how the new versioning mechanism works. Enjoy versioning…","categories":[{"name":".NET","slug":"NET","permalink":"https://kiko.io/categories/NET/"}],"tags":[{"name":"Visual Studio","slug":"Visual-Studio","permalink":"https://kiko.io/tags/Visual-Studio/"},{"name":"Versioning","slug":"Versioning","permalink":"https://kiko.io/tags/Versioning/"},{"name":"T4","slug":"T4","permalink":"https://kiko.io/tags/T4/"}]},{"title":"Automatic Header Images in Hexo","subtitle":"Use static images randomly for posts via Hexo script","series":"A New Blog","date":"2020-06-22","updated":"2020-06-22","path":"post/Automatic-Header-Images-in-Hexo/","permalink":"https://kiko.io/post/Automatic-Header-Images-in-Hexo/","excerpt":"Every article in this blog has an individual header image, to bring a little bit color into it. In this post I will show you how I deal with the images in using and automatic provisioning. For serving these pictures I use a static folder, as described in A New Blog: Customizing Hexo. The hard work is done by the plugin Hexo Generator Copy, which copies the static files into the public_dir while generating.","keywords":"article blog individual header image bring bit color post show deal images automatic provisioning serving pictures static folder customizing hexo hard work plugin generator copy copies files public_dir generating","text":"Every article in this blog has an individual header image, to bring a little bit color into it. In this post I will show you how I deal with the images in using and automatic provisioning. For serving these pictures I use a static folder, as described in A New Blog: Customizing Hexo. The hard work is done by the plugin Hexo Generator Copy, which copies the static files into the public_dir while generating. Static File StructureIt is always advisable to provide one image for every device class, in order to save bandwidth and make the page loading as fast as possible: 1234567891011| static/ | photos/ | mobile/ | my-lovely-picture.jpg | ... | tablet/ | my-lovely-picture.jpg | ... | normal/ | my-lovely-picture.jpg | ... The mobile images are at least 480 pixels wide, the tablet variants 768 pixels and the standard or normal one 1280 pixels. While creating the JPG files, it is important to compress them with a tool like JPEGMini to save data while loading. BindingIn order to bind a picture with some additional information to an article, I have extended the Frontmatter of every post: 1234photograph: file: 'my-lovely-image.jpg' name: 'My Lovely Image' link: 'https://500px.com/photo/123456789/My-Lovely-Image' Usage in ThemeIt relies on your Hexo theme, how to use a header image. In my theme (derived from the standard theme) I just added following code in the article.js to show the individual header image as a background image at the top of the article: 123456789101112131415161718192021222324252627<% if (!index && post.photograph){ %><style> #banner { background-size: cover; } @media screen and (max-width: 479px) { #banner { background-image: linear-gradient(to bottom, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 75%), url("/photos/mobile/<%= post.photograph.file %>"); } } @media screen and (min-width: 480px) and (max-width: 767px) { #banner { background-image: linear-gradient(to bottom, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 75%), url("/photos/tablet/<%= post.photograph.file %>"); } } @media screen and (min-width: 768px) { #banner { background-image: linear-gradient(to bottom, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 75%), url("/photos/normal/<%= post.photograph.file %>"); } }</style><script> var photoLink = document.getElementById("header-photo-link"); photoLink.href = "<%= post.photograph.link%>"; photoLink.innerHTML = "see <strong><%= post.photograph.name%></strong> at 500px";</script><% } %> Important part here is the use of the Frontmatter data post.photograph.file in the URL of the background CSS. The script fills the additional information into the absolute positioned element header-photo-link which is placed on top of the header. Pooling ImagesAs it is time consuming to generate the necessary images, I have created another static folder pool to store prepared files and a text file with the additional information about the image. The structure of pool is different to photos, because of my image workflow and some limitations of automating the provisioning. 12345678| static/ | pool/ | my-lovely-picture/ | meta.txt | mobile.jpg | normal.jpg | tablet.jpg | ... The meta.txt is a simple text file with two lines of text: first the name of the image and second the Url to link to, which will be inserted in the appropriate Frontmatter fields on creating a new post: 12My Lovely Imagehttps://500px.com/photo/123456789/My-Lovely-Image Automate binding and provisioning on new postDevelopers are lazy and I do not make an exception. Having all these pool images and the meta informations, it would be nice, if Hexo just picks and processes one of the pool folders automatically, when I’m creating a new post by calling hexo new "My shiny new post" … and it was easier then I thought. Where to place the code for the automatismHexo has a great API to write plugins and it is not very difficult to setup a new plugin for this, which can be published to the NPM registry. But it is also possible to extend Hexo’s functionality by using a simple script. All you need is a script folder in the root of your Hexo project. Any JS files which is placed there, will be executed by Hexo. Therefore, lets use a script called \\scripts\\process-photo-on-new.js … Things an automatism should do - Step by Step Hook into the creation of a post Pick randomly one of the pool images Place the content of the meta.txt in the Frontmatter Move the 3 device-dependend images into the photos folder Step 1 - Hook into the creation of a postThe needed event, the automatism can hook on, is: 123hexo.on('new', function(data){ //}); It will be executed every time you call the hexo new command. The parameter data is an object with two fields: pathFull path to the MD file of the new post contentComplete content of the scaffold (template), which Hexo has used to create the new post; default is /scaffolds/post.md. By preloading the Hexo Front matter library and parsing data.content we get access to the definition of the new post: 1234567const front = require('hexo-front-matter');hexo.on('new', function(post){ // parse article content var post = front.parse(data.content);}); Step 2 - Pick randomly one of the pool imagesThere are some build-in variables to get the full path, for example, of the source folder, we can use to define the needed paths to the pool and the photo folder. 123456789const front = require('hexo-front-matter');hexo.on('new', function(post){ var post = front.parse(data.content); // set the path variables var poolDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "pool"; var photosDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "photos";}); Next, we need to preload the Hexo FS library for file access, to list the content of the poolDir, including the subfolders, and filter out the meta files. Out of the resulting array we pick one randomly, to use for the new post: 123456789101112131415161718192021const front = require('hexo-front-matter');const fs = require('hexo-fs');hexo.on('new', function(post){ var post = front.parse(data.content); var poolDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "pool"; var photosDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "photos"; // list all files var files = fs.listDirSync(poolDir); // filter the list to get meta files of each subfolder var metaFiles = files.filter(file => file.match(/.*[\\\\]meta.txt/g)); // pick one randomly var metaFile = metaFiles[Math.floor(Math.random() * metaFiles.length)]; // get the name of the picked photo (foldername) var photoName = metaFile.split("\\\\")[0];}); Step 3 - Place the content of the meta.txt in the FrontmatterNow we have to read the meta file, place the data in the Frontmatter and save the article file: 123456789101112131415161718192021222324252627282930const front = require('hexo-front-matter');const fs = require('hexo-fs');hexo.on('new', function(post){ var post = front.parse(data.content); var poolDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "pool"; var photosDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "photos"; var files = fs.listDirSync(poolDir); var metaFiles = files.filter(file => file.match(/.*[\\\\]meta.txt/g)); var metaFile = metaFiles[Math.floor(Math.random() * metaFiles.length)]; var photoName = metaFile.split("\\\\")[0]; // read meta file var meta = fs.readFileSync(poolDir + "\\\\" + metaFile); var metas = meta.split("\\n"); // place file and additional info in the Frontmatter post.photograph.file = photoName + ".jpg"; post.photograph.name = metas[0]; post.photograph.link = metas[1]; // convert content back postStr = front.stringify(post); postStr = '---\\n' + postStr; // store article fs.writeFile(data.path, postStr, 'utf-8');}); Step 4 - Move the 3 device-dependend images into the photos folderLast but not least, we have to move the pool images into the photos folder and remove the pool folder with all its processed content: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051const front = require('hexo-front-matter');const fs = require('hexo-fs');hexo.on('new', function(post){ var post = front.parse(data.content); var poolDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "pool"; var photosDir = hexo.source_dir.replace("\\source", hexo.config.static_dir) + "photos"; var files = fs.listDirSync(poolDir); var metaFiles = files.filter(file => file.match(/.*[\\\\]meta.txt/g)); var metaFile = metaFiles[Math.floor(Math.random() * metaFiles.length)]; var photoName = metaFile.split("\\\\")[0]; var meta = fs.readFileSync(poolDir + "\\\\" + metaFile); var metas = meta.split("\\n"); post.photograph.file = photoName + ".jpg"; post.photograph.name = metas[0]; post.photograph.link = metas[1]; postStr = front.stringify(post); postStr = '---\\n' + postStr; fs.writeFile(data.path, postStr, 'utf-8'); //copy normal image fs.copyFile( poolDir + "\\\\" + photoName + "\\\\normal.jpg", photosDir + "\\\\normal\\\\" + photoName + ".jpg", function() { //copy tablet image fs.copyFile( poolDir + "\\\\" + photoName + "\\\\tablet.jpg", photosDir + "\\\\tablet\\\\" + photoName + ".jpg", function() { //copy mobile image fs.copyFile( poolDir + "\\\\" + photoName + "\\\\mobile.jpg", photosDir + "\\\\mobile\\\\" + photoName + ".jpg", function() { //remove processed pool folder fs.rmdirSync(poolDir + "\\\\" + photoName); }); }); });}); Now it so easy to write a new post, because almost everything is set and I can concentrate on the article. Also, it is a nice surprise to see, which photo the script has chosen. The only thing I have to do from time to time, is to refill the pool folder with new images.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"Localization with resource files in JavaScript web apps","subtitle":"How to work with Visual Studio resource files for localization in Single Page Applications","date":"2020-06-13","updated":"2020-06-13","path":"post/Localization-with-resource-files-in-JavaScript-web-apps/","permalink":"https://kiko.io/post/Localization-with-resource-files-in-JavaScript-web-apps/","excerpt":"There are plenty of editors out there to help you writing JavaScript web applications. As I’m working in my daily life with Visual Studio, it is a obvious choice for me. One of the most time saving tools in VS is the plugin ResXManager, which is an awesome assistant on managing the translations for a Desktop- or ASP.NET-App, which uses XML-based RESX files.","keywords":"plenty editors writing javascript web applications im working daily life visual studio obvious choice time saving tools plugin resxmanager awesome assistant managing translations desktop- aspnet-app xml-based resx files","text":"There are plenty of editors out there to help you writing JavaScript web applications. As I’m working in my daily life with Visual Studio, it is a obvious choice for me. One of the most time saving tools in VS is the plugin ResXManager, which is an awesome assistant on managing the translations for a Desktop- or ASP.NET-App, which uses XML-based RESX files. Mostly very localization is based on key/value pairs, defined in separate files for every language provided. Implementing several languages in pure JavaScript apps is a little more difficult, because it makes no sense to deal with big XML files in JS. All localization libraries in the market uses JSON for storing the translations and it is a little bit of work to find the right one for your requirements. Localization in JavaScriptFor a current project I use jquery-lang, because it provides the switch of the apps UI language without reloading and it is easy to implement. Thanks Rob Evans for your work… The definition of “tokens” in one JSON file for each language is quite easy: ../languages/en.json12345{ "token": { "my-test": "My Test in English" }} ../languages/de.json12345{ "token": { "my-test": "Mein Test in Deutsch" }} The usage also: 1<div lang="en" data-lang-token="my-test"> Using RESX and convert to JSON on buildHaving this, the most time consuming work is to enter the translations to the localization files. If you have hundreds of them, it is hard to keep the 2, 3 or more language files in sync. You need a helper… And here comes ResXManager to the rescue, if you work with VS … but it needs a conversation from RESX to the JSON format jquery-lang uses and this a task, which can be done on building the JS app, by using a task runner like Grunt. As there was no Grunt plugin/task out there to fit my needs, I have created grunt-resource2json (GitHub, NPM). The configuration in the gruntfile.js is like: gruntfile.js12345678910111213141516171819202122grunt.initConfig({ resource2json: { convert: { options: { format: "jquery-lang" }, files: [ { input: "resources/Resource.resx", output: "build/langpacks/en.json" }, { input: "resources/Resource.de-DE.resx", output: "build/langpacks/de.json" }, { input: "resources/Resource.es-ES.resx", output: "build/langpacks/es.json" } ] } }); It takes one RESX file (input) and converts it to a JSON file (output) in an array of files. The heavy work in the plugin is done by the library xml2js, which transforms the complete XML of the RESX file into a JSON object in one call. All I had to do, was to write all DATA nodes in a loop into the jquery-lang given structure and save it as JSON. Currently supported is the format for jquery-lang only, but it would be awesome, if you fork the code on GitHub and send me a Pull Request with the implementation of your needed format.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Visual Studio","slug":"Visual-Studio","permalink":"https://kiko.io/tags/Visual-Studio/"},{"name":"Resource","slug":"Resource","permalink":"https://kiko.io/tags/Resource/"},{"name":"Localization","slug":"Localization","permalink":"https://kiko.io/tags/Localization/"}]},{"title":"TFS/DevOps: Delete Remote Workspace","subtitle":null,"date":"2020-02-27","updated":"2020-02-27","path":"post/TFS-DevOps-Delete-Remote-Workspace/","permalink":"https://kiko.io/post/TFS-DevOps-Delete-Remote-Workspace/","excerpt":"If you are working with freelance developers and Azure DevOps/TFS with TFVC (Team Foundation Version Control) in your company, maybe this will look familiar to you: You hire a new freelancer and you want to reuse the hardware, including the complete software setup, to bring him/her to work as fast and straightforward as possible. You set up a new Azure Devops account with all necessary permissions and you think you’re done. No you are not…","keywords":"working freelance developers azure devops/tfs tfvc team foundation version control company familiar hire freelancer reuse hardware including complete software setup bring him/her work fast straightforward set devops account permissions youre not…","text":"If you are working with freelance developers and Azure DevOps/TFS with TFVC (Team Foundation Version Control) in your company, maybe this will look familiar to you: You hire a new freelancer and you want to reuse the hardware, including the complete software setup, to bring him/her to work as fast and straightforward as possible. You set up a new Azure Devops account with all necessary permissions and you think you’re done. No you are not… Everytime a user connects to a Team Project on Azure DevOps via Visual Studio and gets the code, VS is creating a remote workspace on the server, with the machine name as default, therefor it is not enough to wipe the profile and any other legacies of the last user from the machine. You also have to remove the remote workspace. Otherwise you will get an error message like that, if you are using a unique file structure on the developers hard disc: 1The working folder c://xxx is already in use by the workspace yyy;zzz on computer yyy The variable xxx stands for the blocked folder, yyy for the workspace/machine name and zzz for the users id on Azure DevOps. Unfortunately, there is no visual management console on Azure DevOps to manage your server workspaces, but there is a command line tool called tf.exe. The easiest way to get rid of the unused server workspace in 3 steps: Step 1Run Developer Command Prompt with Administrator privileges from Visual Studio 2019 and login with your Azure DevOps credentials. If the Login dialog doesn’t show up, force it by executing: 1tf.exe workspace Step 2Get a list of all remote workspaces available in your DevOps Collection by running the command: 1tf.exe workspaces /computer:* /owner:* /format:xml > c:\\temp\\workspaces.xml You can get a list of all your workspaces by running tf workspaces, but the list only shows you the owner, but not the necessary ownerid and … it is nicer to have a file to search in. Step 3Find the abandoned workspace in the list and note its name and ownerid for running the command: 1tf workspace /delete {WORKSPACE.name};{WORKSPACE.ownerid} Now your new colleague can create his own workspace on the same machine. UpdateIn case you want to switch your own DevOps account to another and use the same folder as before, you can certainly delete the local workspace, but this wont help, because you are still logged in at TeamExplorer and the folder knows to whom it belongs. Solution is easy: Quit Visual Studio Rename folder in ***_OLD or something Create new folder with the same name Enter C:\\Users\\YOUR-NAME\\appdata\\Local\\Microsoft\\Team Foundation\\VS-VERSION\\Cache and emtpy the folder to let Visual Studio forget who you are Remove all your Remote Workspaces as described above Start Visual Studio, connect in TeamExplorer to your TFS server and map the code to your folder More Info Microsoft Docs: Use Team Foundation version control commandsStackoverflow: How to remove TFS workspace mapping for another user","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Visual Studio","slug":"Visual-Studio","permalink":"https://kiko.io/tags/Visual-Studio/"},{"name":"TFS/DevOps","slug":"TFS-DevOps","permalink":"https://kiko.io/tags/TFS-DevOps/"}]},{"title":"Better Input Change Event","subtitle":null,"date":"2019-11-26","updated":"2019-11-26","path":"post/Better-Input-Change-Event/","permalink":"https://kiko.io/post/Better-Input-Change-Event/","excerpt":"Often it is important to trigger an event, after the user of your website/web app has filled out an text input. You have to do something with the given value in JavaScript. The intended event for this is change, which will be triggered, when the user has ended changing by leaving the input with his cursor, mostly by using the TAB key. This works at some degree, if there is a physical keyboard, but not really on mobile devices … and for me is leaving the field often too late to start the upcoming event.","keywords":"important trigger event user website/web app filled text input javascript intended change triggered ended changing leaving cursor tab key works degree physical keyboard mobile devices … field late start upcoming","text":"Often it is important to trigger an event, after the user of your website/web app has filled out an text input. You have to do something with the given value in JavaScript. The intended event for this is change, which will be triggered, when the user has ended changing by leaving the input with his cursor, mostly by using the TAB key. This works at some degree, if there is a physical keyboard, but not really on mobile devices … and for me is leaving the field often too late to start the upcoming event. A better way to show the user the result of his entered value, could be the event input which fires on every key stroke, but could be way to often, if the triggered event is for example an AJAX call. Best solution is, to observe the users key strokes and trigger the event, when he stops typing. Then there is no extra action needed by the user and the event isn’t triggered multiple times. Here’s an implementation with jQuery: 12345678910$("#my-text-input").keyup(function () { var $this = $(this); clearTimeout($.data(this, 'timer')); var wait = setTimeout(function () { //do something with the value... }, 1000); $(this).data('timer', wait);}); Important is to wipe and set the timer on every key up, to achive that the event will be executed after 1 second after the last key stroke only.","categories":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"}],"tags":[{"name":"jQuery","slug":"jQuery","permalink":"https://kiko.io/tags/jQuery/"}]},{"title":"Hexo and the Dark Mode ... revised","subtitle":"Second approach to implement 'prefers-color-scheme'","series":"A New Blog","date":"2019-10-26","updated":"2019-10-26","path":"post/Hexo-and-the-Dark-Mode-revised/","permalink":"https://kiko.io/post/Hexo-and-the-Dark-Mode-revised/","excerpt":"While writing my post Hexo and the Dark Mode a few days ago, I thought it would be nice, if I could switch between the normal (light) and the dark theme, I’ve created for the support of the OS-related Dark Mode, even manually. The only thing I needed was a toggle element and a little bit of JavaScript. Of course, I couldn’t manipulate the media query prefers-color-scheme itself, but introduce a different way by blog uses it. Instead of implementing the media query directly into my CSS (or Stylus) code, I used a root selector, which can be manipulated by JavaScript … something like this: 12345678910body { background-color: white; color: black;}[data-theme="dark"] body { background-color: black; color: white; }}","keywords":"writing post hexo dark mode days ago thought nice switch normal light theme ive created support os-related manually thing needed toggle element bit javascript couldnt manipulate media query prefers-color-scheme introduce blog implementing directly css stylus code root selector manipulated … 12345678910body { background-color white color black}[data-theme="dark"] body black }}","text":"While writing my post Hexo and the Dark Mode a few days ago, I thought it would be nice, if I could switch between the normal (light) and the dark theme, I’ve created for the support of the OS-related Dark Mode, even manually. The only thing I needed was a toggle element and a little bit of JavaScript. Of course, I couldn’t manipulate the media query prefers-color-scheme itself, but introduce a different way by blog uses it. Instead of implementing the media query directly into my CSS (or Stylus) code, I used a root selector, which can be manipulated by JavaScript … something like this: 12345678910body { background-color: white; color: black;}[data-theme="dark"] body { background-color: black; color: white; }} In every Stylus file, where I used @media prefers-dark to achieve the automatic switch by the OS, I changed this line into /[data-theme="dark"] & : 12345678#mobile-nav-header background-color: color-background /[data-theme="dark"] & background-color: dark-color-background img.avatar ... /[data-theme="dark"] & filter: brightness(85%) Some explanations on the Stylus syntax: / means the root of the DOM and & points to the parent selector. Therefore the example will be rendered into this: 12345678910111213#mobile-nav-header { background-color: #f1f1f1;}[data-theme="dark"] #mobile-nav-header { background-color: #111;}#mobile-nav-header img.avatar {...}[data-theme="dark"] #mobile-nav-header img.avatar filter: brightness(85%);} Only problem was: the “Root + Parent” Stylus selector doesn’t work in the block variables in the _extend.styl. So I had to copy all theme relevant styles directly to the elements, where such a block was used: @extend <block-name>. The Toggle SwitchIn the footer.ejs I added a toggle checkbox, where I could bind my JavaScript… 1234<div id="footer-theme"> <input type="checkbox" id="theme-switch"> <label for="theme-switch"></label></div> … and some CSS in the footer.styl, to style it: 12345678910111213141516171819202122input#theme-switch[type=checkbox] { display:none;}input#theme-switch[type=checkbox] + label height: 16px width: 16px display: inline-block padding: 12px font-size: 22px cursor: pointer &:before display: inline-block font-size: inherit text-rendering: auto -webkit-font-smoothing: antialiased font-family: fa-icon-solid content: icon-mooninput#theme-switch[type=checkbox]:checked + label &:before content: icon-sun The icon variables are defined in the _variables.styl like this: 12icon-moon = "\\f186"icon-sun = "\\f185" The JavaScriptEverything was now prepared to implement the switching code in JavaScript, which should support a manual switch by clicking the toggle element as well as the automatic switch by the OS. I wrapped all necessary code into a seperate JS file and placed a reference in the after-footer.ejs, which places it at the bottom of the HTML: 1<%- js('js/dark-mode-toggle.js') %> 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758function detectColorScheme() { var theme = "light"; //default // get last used theme from local cache if(localStorage.getItem("theme")){ if(localStorage.getItem("theme") === "dark"){ theme = "dark"; } } else if(!window.matchMedia) { // matchMedia not supported return false; } else if(window.matchMedia("(prefers-color-scheme: dark)").matches) { // OS has set Dark Mode theme = "dark"; } // set detected theme if (theme === "dark") { setThemeDark(); } else { setThemeLight(); }}const toggleTheme = document.querySelector('input#theme-switch[type="checkbox"]');function setThemeDark() { localStorage.setItem('theme', 'dark'); document.documentElement.setAttribute('data-theme', 'dark'); toggleTheme.checked = true;}function setThemeLight() { localStorage.setItem('theme', 'light'); document.documentElement.setAttribute('data-theme', 'light'); toggleTheme.checked = false;}// Listener for theme change by toggletoggleTheme.addEventListener('change', function(e) { if (e.target.checked) { setThemeDark(); } else { setThemeLight(); }}, false);// Listener for theme change by OSvar toggleOS = window.matchMedia('(prefers-color-scheme: dark)');toggleOS.addEventListener('change', function (e) { if (e.matches) { setThemeDark(); } else { setThemeLight(); }});// call theme detectiondetectColorScheme(); By using the both addEventListener‘s, each switch will be recognized and this approach is capable to support even more themes, just by using different values in the data-theme attribute.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Stylus","slug":"Stylus","permalink":"https://kiko.io/tags/Stylus/"},{"name":"Dark Mode","slug":"Dark-Mode","permalink":"https://kiko.io/tags/Dark-Mode/"}]},{"title":"Hexo and the Dark Mode","subtitle":"First approach to implement 'prefers-color-scheme'","series":"A New Blog","date":"2019-10-23","updated":"2019-10-23","path":"post/Hexo-and-the-Dark-Mode/","permalink":"https://kiko.io/post/Hexo-and-the-Dark-Mode/","excerpt":"Due to the fact, that nowadays everybody is talking about Dark Modes for Browsers and Operating Systems, in order to save battery or for easier reading (uhh, really?), I decided my blog should support that.","keywords":"due fact nowadays talking dark modes browsers operating systems order save battery easier reading uhh decided blog support","text":"Due to the fact, that nowadays everybody is talking about Dark Modes for Browsers and Operating Systems, in order to save battery or for easier reading (uhh, really?), I decided my blog should support that. Starting point is the new media query prefers-color-scheme, which is actually supported by all modern browsers. TechniqueMy first read was Tom Brow’s Dark mode in a website with CSS, where he shows how to use the media query. Simplified, this is it, assuming the light version is the default: 1234567891011body { background-color: white; color: black;}@media (prefers-color-scheme: dark) { body { background-color: black; color: white; }} Pimping CSS for automatic switchingTo support the automatic browser/OS-based automatic switch in Hexo, where Stylus is used, I had to change some template files. First the _variables.styl: 1234567891011121314// existing color variablescolor-background = #f1f1f1color-foreground = #111color-border = #ddd...// new dark color variablesdark-color-background = #111dark-color-foreground = #eeedark-color-border = #000...// new media query variableprefers-dark = "(prefers-color-scheme: dark)" Next step was to change the _extend.styl, where some Stylus variables are defining complete blocks to extend. Here I had to supplement all lines, where something mode-dependend was defined, by adding the new prefers-dark media query and beneath the new ‘dark’ equivalence of the style: 12345678910111213141516171819$base-style hr ... border: 1px dashed color-border-article @media prefers-dark border: 1px dashed dark-color-border-article ...$block ... background: color-block box-shadow: 1px 2px 3px color-border border: 1px solid color-border @media prefers-dark background: dark-color-block box-shadow: 1px 2px 3px dark-color-border border-color: dark-color-border... The same changes I had to do in every template styl file, where one of the colors or other mode dependent style was used. For example: 12345678#mobile-nav-header background-color: color-background @media prefers-dark background-color: dark-color-background img.avatar ... @media prefers-dark filter: brightness(85%) This will be rendered as: 123456789101112131415#mobile-nav-header { background-color: #f1f1f1;}@media (prefers-color-scheme: dark) { #mobile-nav-header { background-color: #111; }}#mobile-nav-header img.avatar { ...}@media (prefers-color-scheme: dark) { filter: brightness(85%);} Please note the use of filter:brightness() in the example. It is always advisable to darken the images too, because they can really pop out on dark backgrounds.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Stylus","slug":"Stylus","permalink":"https://kiko.io/tags/Stylus/"},{"name":"Dark Mode","slug":"Dark-Mode","permalink":"https://kiko.io/tags/Dark-Mode/"}]},{"title":"A New Blog: Blogging and Synching en route","subtitle":"Part Three of having fun with Hexo and GitHub Pages","series":"A New Blog","date":"2019-09-30","updated":"2019-09-30","path":"post/A-New-Blog-Blogging-and-Synching-en-route/","permalink":"https://kiko.io/post/A-New-Blog-Blogging-and-Synching-en-route/","excerpt":"I work with several devices, some Windows, some Android, and sometimes I have time to write on my articles at home (Notebook, Tablet), in my spare time in the office (Desktop, Laptop) or on my way to somewhere (Smartphone). Right now I’m am in a barber shop, waiting for my haircut and write these lines. So, wherever I am, I need the Hexo project locally, but in sync on a digital device. The blog is synced via Dropbox, but hosted on GitHub Pages, so on every device I need the publishing functions of Git too.","keywords":"work devices windows android time write articles home notebook tablet spare office desktop laptop smartphone im barber shop waiting haircut lines hexo project locally sync digital device blog synced dropbox hosted github pages publishing functions git","text":"I work with several devices, some Windows, some Android, and sometimes I have time to write on my articles at home (Notebook, Tablet), in my spare time in the office (Desktop, Laptop) or on my way to somewhere (Smartphone). Right now I’m am in a barber shop, waiting for my haircut and write these lines. So, wherever I am, I need the Hexo project locally, but in sync on a digital device. The blog is synced via Dropbox, but hosted on GitHub Pages, so on every device I need the publishing functions of Git too. Sync Hexo ProjectBest option for me to achieve this was Dropbox. Another benefit on that is: I can work on the structure of the blog wherever I am and commit when the new feature or improvement is done, because all Git related files are always in sync too. Writing, Editing and Publishing on WindowsMy preferred editor is Visual Studio Code. Good file handling, easy writing, full Git integration and tons of other plugins and helpers. Chapeau Microsoft, well done. Some of the following VS Code plugins makes working with Hexo on GitHub pages a breeze: Adds Hexo commands like init, new, generate, server and clean to the VS Code command palette. Keyboard shortcuts for basic formatting, automatic list editing, autocomlete for images, table formatter and much more for an easier handling of Markdown. Markdown linting and style checking Adds syntax highlighting and code completion to Stylus files Complete visual management of your repositories in VS Code View a Git Graph of your repository with all changes and manage commits. With this editor and its helpers, I’m just two clicks away from publishing a new article or even a new version of the Hexo blog itself. Writing on AndroidThere are a lot of Markdown editors available on Google Play, but one is outstanding: iA Writer for Android. I can open my posts or drafts directly from Dropbox, without the need of any sychronization. Open, write, close, done. Publishing on AndroidThere are some Git related Android apps out there, but no solution was satisfying. Furthermore, I didn’t really need Git here, because I didn’t want to have all source files on my smartphone. I’m working directly on the Dropbox stored MD files via iA Writer. Finally and most important, Git won’t be enough, because before publishing, I have to run hexo generate! Therefore some sort of automatic transfer from Dropbox to GitHub is also out of the game. What I needed, was to tell a server at a certain point of time ‘Hey, please publish for me’, using the only connection I have: Dropbox. Introducing a DemonI have a little media server, running on Windows, and he is synchronizing some folders with Dropbox. He could do the job! After I installed all necessary packages, like NodeJS, Hexo and Git, I included the project folder into the sync. Next step was to design a so called Hexo Command File, a simple TXT file, which holds commands in single lines, extended with execution times, when they were successfully running. 12345postdraft: A-New-Blog-Blogging-and-Synching-en-routepublishnewdraft: "A New Blog: Blogging and Synching en route" @ 2019-09-30 21:15regenerate @ 2019-09-29 16:40:01publish @ 2019-09-29 16:40:10 These commands are predefined, because they bundle several real commands and I didn’t want to deal with real commands, due to security reasons. The unprocessed commands are standing at the top of the file (in execution order!) and parameters are separated from the command by a colon and delimited by commas. <command>: [<param1>, ...] @ <execution time> Next step was to create a program to work as an executing demon, who monitors the Hexo Command File (synced by Dropbox) on my server and executes commands without execution dates. I decided to create a simple Console Application in C# and use the built-in Windows Task Scheduler for running it every 2 minutes. The application is called HexoCommander and is available at GitHub. It expects the Hexo Command File to be named hexo-commands.txt, located in the same folder, and provides the following commands: newdraft: “<title>” … runs hexo new draft "<title>" Creates a new draft. postdraft: “<filename without extension>” … runs hexo publish "<filename without extension>" Makes a post out of a draft. regenerate … runs hexo clean hexo generate Wipes all Hexo static pages and generates them new. publish … runs hexo generate git add "source/*" "docs/*" git commit -m "Remote publication via HexoCommander" git push origin master Generates Hexo static pages, stage changes on drafts, posts and static pages, commits the changes with a generic message and pushes them to the server. Running the demonI would have never expected, that the trickiest part was to get HexoCommander running via Windows Task Scheduler. What a mess! I finally find the solution here: Compile HexoCommander in a x86 configuration Create a new task in Task Scheduler with Trigger Dialy Recur every 1 days Repeat task every 2 minutes for a duration of 1 day Action Program/Script: %systemroot%\\Syswow64\\cmd.exe Add Arguments: /C “C:\\MyPath\\HexoCommander.exe /workdir=C:\\MyPath” Start In: %systemroot%\\Syswow64\\ Because some executing commands in the chain are NOT 64-bit, I had to force Task Scheduler to run the 32-bit Command Shell in its own path (see ‘Start In’ and don’t forget the closing backslash) and take the 32-bit compiled HexoCommander as argument after the parameter /C (forcing command to terminate), including its own argument for defining the real working directory. Mind bending, but works…","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"A New Blog: Customizing Hexo","subtitle":"Part Two of having fun with Hexo and GitHub Pages","series":"A New Blog","date":"2019-09-25","updated":"2019-09-25","path":"post/A-New-Blog-Customizing-Hexo/","permalink":"https://kiko.io/post/A-New-Blog-Customizing-Hexo/","excerpt":"Hexo is a great tool to get quick results (see Part One of this series), when you decide to have a blog and its defaults are practical, but it’s power lies in the possiblities of customization via plugins. On the official plugin page, there are actually 302 plugins listed, but there are many more and no wish will be unsatisfied. I will show you which of these I found worth to work with…","keywords":"hexo great tool quick results part series decide blog defaults practical power lies possiblities customization plugins official plugin page listed unsatisfied show found worth work with…","text":"Hexo is a great tool to get quick results (see Part One of this series), when you decide to have a blog and its defaults are practical, but it’s power lies in the possiblities of customization via plugins. On the official plugin page, there are actually 302 plugins listed, but there are many more and no wish will be unsatisfied. I will show you which of these I found worth to work with… Relative Image PathThe build-in way to include images in your posts works fine, but it is a little aside the normal way to declare images in Markdown. The plugin [Hexo Asset Link] corrects that. After installing via npm install hexo-asset-link --save you can write this: ![Test Image](hello-world/image-1.png) The best is, that VS Code’s Markdown can now show the image. UPDATE:Actually the plugin destroys external links, so don’t use it until this is fixed … or go to node_modules > hexo-asset-link > index.js in your project and change in line 22 protocal to protocol. UPDATE from Update:liolok, the author of the plugin has merged my pull request and published a new new version without the typo. It works now as expected. Hide PostsA new Hexo project comes with a sample post called Hello World. This is fine to play around with, but you don’t want to publish it. Here comes a Hexo plugin to the rescue called Hexo Hide Posts. After installing, you just have to write hidden: true to the Front Matter of you post and it won’t be shown on the blog, but it is still available by URL. Static FilesHexo has the concept of Assets Folders, but for static files, beside article based files, I find it more useful to have a STATIC folder and copy the contents on every build into the publish folder. A good helper for this approach is the plugin Hexo Generator Copy. Install it by running npm install hexo-generator-copy --save and add static_dir: static to your _config.yml and you are done. ![Hexo Static Files](/post/A-New-Blog-Customizing-Hexo/vscode-1.png) FeedThe default Hexo layout has an Atom Feed icon in the upper right corner, but strangely no feed file is generated on build. You need to install the plugin Hexo Feed Generator to fix this, by running npm install hexo-generator-feed --save and copy following section into the _config.yml: 123456789feed: type: atom path: atom.xml limit: 20 hub: content: content_limit: 140 content_limit_delim: ' ' order_by: -date Manifest for PWAIn these modern times it’s always a good idea, that your blog feels like an App. For this you need a manifest file (JSON) an several icons (PNG). You can generate these files very fast with the Web App Manifest Generator and store it in your static folder. To bind this file into your blog, you can use the plugin Hexo PWA. Run npm install --save hexo-pwa and copy following section to your _config.yml, where you take the settings from your generated manifest file: 1234567891011121314151617pwa: manifest: path: /manifest.json body: name: myblog.de short_name: My Blog icons: - src: /images/icon-192x192.png sizes: 192x192 type: image/png - src: /images/icon-512x512.png sizes: 512x512 type: image/png start_url: /index.html theme_color: '#025ab1' background_color: '#dddddd' display: standalone Sitemap FileTo help Google and others a bit to index your blog, it is advisable to provide a sitemap file. Here comes Hexo Generator Sitemap to the rescue. Install it by running the command npm install hexo-generator-sitemap --save. You can configure it via _config.yml: 123sitemap: path: sitemap.xml template: ./sitemap-template.xml The plugin installation doesn’t create the needed sitemap-template file, so be sure you grab a copy from the plugins repository: https://github.com/hexojs/hexo-generator-sitemap/blob/master/sitemap.xml CommentingHexo doesn’t have a commenting system, but it’s prepared to stick Disqus comments under each article. Just create a new Disqus account for your blog and note the given short name. By adding following section to the _config.yml Hexo shows the commenting section: 12disqus_enabled: truedisqus_shortname: my-blog Inifinite ScrollHexo shows as much articles at the start page as configured in _config.yml under index_generator.per_page, but it’s nicer to load more articles as you scroll by using the Hexo script Inifinite Scroll. Install by adding following little script in themes & gt; layout > _partial > after-footer.ejs 12345678<script src="//cdn.jsdelivr.net/gh/frontendsophie/hexo-infinite-scroll@2.0.0/dist/main.js"></script> <script> infiniteScroll({ showNum: 5, style: 'line-scale', color: '#025ab1' })</script> Back To TopIts nice to support the reader on scolling by providing a Scroll-To-Top button. The easiest way to get this, is the script Vanilla Back To Top. Just add follwing to themes >layout > _partial > after-footer.ejs: 123456789101112131415<script>addBackToTop({ diameter: 30, backgroundColor: 'rgb(0, 90, 180)', textColor: '#fff'})</script><style>#back-to-top { border-radius: 0; opacity: 0.6;}#back-to-top:hover { opacity: 1;}</style>","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"A New Blog: VS Code, Hexo and GitHub Pages","subtitle":"Part One of having fun with Hexo and GitHub Pages","series":"A New Blog","date":"2019-09-23","updated":"2019-09-23","path":"post/A-New-Blog-VS-Code-Hexo-and-GitHub-Pages/","permalink":"https://kiko.io/post/A-New-Blog-VS-Code-Hexo-and-GitHub-Pages/","excerpt":"A few days ago I puzzled over a technical problem regarding SQL Server, Active Directory and Visual Studio Database Projects. With tips, hints and snippets from several websites I got it running and the solution was absolutely memorable. For myself and for others. Nothing is harder than to know ‘you did this before…’, but not to remember how. Because of this strong leaning towards oblivion, I started over 20 years ago my very first website zerbit.de, manually crafted with Classic ASP and a SQL Server database as backend, with an editor, tagging, categories and so on. It was really exciting to build this blog from scratch and writing articles for it, but it was so time consuming to expand the features of the website and keep it running, that some day I quit it silently. So, to document the solution mentioned above, I could use tools like OneNote or others, like in the past years, but this would be just for me and not for all developers, who have a similar problem. I felt it would be unfair to participate from the knowledge of others to get my solution and dont give something back. I decided to write an article just in HTML and publish it on my personal GitHub Page that I didn’t used so far. Ok, just Text … ugly. Just a little CSS and a little more structure and maybe I should do something with Vue JS … STOP … It would be better to pick one of the cool new static website generators based on Node.js, to detain myself from inventing the wheel again and save my time to write articles. So I did a little research and found HEXO … Bingo! I can work with my favorite editor Visual Studio Code, its all HTML, JavaScript and CSS, I can write my articles in Markdown and Hexo has a lot of helpers for stuff Markdown doesn’t provide, it produces static files and works only with files, therefore no need for a database … and it is well documented.","keywords":"days ago puzzled technical problem sql server active directory visual studio database projects tips hints snippets websites running solution absolutely memorable harder before… remember strong leaning oblivion started years website zerbitde manually crafted classic asp backend editor tagging categories exciting build blog scratch writing articles time consuming expand features day quit silently document mentioned tools onenote past developers similar felt unfair participate knowledge dont give back decided write article html publish personal github page didnt text … ugly css structure vue js stop pick cool static generators based nodejs detain inventing wheel save research found hexo bingo work favorite code javascript markdown lot helpers stuff doesnt provide produces files works documented","text":"A few days ago I puzzled over a technical problem regarding SQL Server, Active Directory and Visual Studio Database Projects. With tips, hints and snippets from several websites I got it running and the solution was absolutely memorable. For myself and for others. Nothing is harder than to know ‘you did this before…’, but not to remember how. Because of this strong leaning towards oblivion, I started over 20 years ago my very first website zerbit.de, manually crafted with Classic ASP and a SQL Server database as backend, with an editor, tagging, categories and so on. It was really exciting to build this blog from scratch and writing articles for it, but it was so time consuming to expand the features of the website and keep it running, that some day I quit it silently. So, to document the solution mentioned above, I could use tools like OneNote or others, like in the past years, but this would be just for me and not for all developers, who have a similar problem. I felt it would be unfair to participate from the knowledge of others to get my solution and dont give something back. I decided to write an article just in HTML and publish it on my personal GitHub Page that I didn’t used so far. Ok, just Text … ugly. Just a little CSS and a little more structure and maybe I should do something with Vue JS … STOP … It would be better to pick one of the cool new static website generators based on Node.js, to detain myself from inventing the wheel again and save my time to write articles. So I did a little research and found HEXO … Bingo! I can work with my favorite editor Visual Studio Code, its all HTML, JavaScript and CSS, I can write my articles in Markdown and Hexo has a lot of helpers for stuff Markdown doesn’t provide, it produces static files and works only with files, therefore no need for a database … and it is well documented. Installation.. is quite easy, as described here: https://hexo.io/docs/setup Create folder and open in VS Code Open VS Code Terminal window Install Hexo with $ npm install -g hexo-cli Init Hexo project with $ hexo init Install dependencies with npm install Done WritingCreate new post/draftHexo has posts and drafts, whereat drafts has to published via a Hexo command to become a post. To create an article use the command hexo new post|draft "My Title". The title will be converted in a URL-encoded string and will be used as file name and url. Meta dataEvery post/draft starts with its header (so called Front Matter) to store some meta data, which describes the post, like title, date, tags or categories. This is used by Hexo to classify and arrange your post during the build. MarkdownHexo posts/drafts are written in Markdown. Good syntax reference are the Markdown Guide and the more detailed Markdown Syntax Guide. ExcerptIs is usual to show a short excerpt an the start page of a blog, to keep it compact and teasering the user to click on a READ MORE button. To achieve this, you just have to add following comment to your article. Everything above is the excerpt and everything below is only shown, when you enter the article: <!-- more --> ImagesSome articles will contain images to illustrate something and the question is, where should they be stored? Answer: In a folder beside the post/draft, which has the same name as the article MD file. To get this, you have to activate the setting post_asset_folder in your _config.yml. Now this folder will be created automatically, when you add a new post/draft. In your Markdown you reference your image with: {% asset_img image-1.png \"Test Image\" %} BuildHexo is a website generator, so a build will generate the whole website in a special folder, which has to be published. This output folder can be configured in the _config.yml: public_dir: public To wipe the output folder, run the command: hexo clean To start the build, run: hexo generate To view the website via the build-in local Hexo server, run: hexo server PublishingMost “complex” task was to publish the new blog on GitHub Pages. My first approach was to use my personal page, as I did with my single HTML file, but this didn’t work, because I wanted to store the whole project on GitHub and it is not possible to point a personal page to the subdirectory docs or use a different branch as master. The simple solution was to create a new repository, named after my my blog kiko.io, to store teh whole project and point the GitHub Page to the subdirectory docs in the settings of the repository. By overriding the default publish folder of Hexo in _config.yml … public_dir: docs … everything was set up. Commit and Push via git and done. Hexo has its own deploying mechanism and it is advisable to disable it, by commenting out the Deployment section _config.yml. Next step was to use my own custom domain for the blog. To achieve this, the most easiest way is to create a text file named CNAME (without extension!) with the content of the domain in a single line and publish this file in the root of the docs folder. Github will recognize this file and do the setup automatically. To point the domain to GitHub, I had to create following A records in my domain providers DNS settings: 185.199.108.153 185.199.109.153 185.199.110.153 185.199.111.153 Last step was to enable Enforce HTTPS in the repositories settings.","categories":[{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"}],"tags":[{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"}]},{"title":"How-To: Visual Studio Database Project and ADSI","subtitle":null,"date":"2019-09-17","updated":"2019-09-17","path":"post/How-To-Visual-Studio-Database-Project-and-ADSI/","permalink":"https://kiko.io/post/How-To-Visual-Studio-Database-Project-and-ADSI/","excerpt":"If you are working with a Visual Studio Database Project and have to deal with data from the Active Directory via a Linked Server, you have to announce the data structure of the AD data in order to get the project compiled.","keywords":"working visual studio database project deal data active directory linked server announce structure ad order compiled","text":"If you are working with a Visual Studio Database Project and have to deal with data from the Active Directory via a Linked Server, you have to announce the data structure of the AD data in order to get the project compiled. Step 1 - Linking to the Active DirectoryFirst of all you have to connect your SQL Server to the AD permanently, by running following SQL script once on your SQL Server: USE [master] GO EXEC master.dbo.sp_addlinkedserver @server = N'ADSI', @srvproduct=N'Active Directory Service Interfaces', @provider=N'ADSDSOObject', @datasrc=N'adsdatasource' EXEC master.dbo.sp_addlinkedsrvlogin @rmtsrvname=N'ADSI', @useself=N'False', @locallogin=NULL, @rmtuser=N'mydomain\\myadminuser', @rmtpassword='mypassword' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'collation compatible', @optvalue=N'false' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'data access', @optvalue=N'true' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'dist', @optvalue=N'false' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'pub', @optvalue=N'false' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'rpc', @optvalue=N'false' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'rpc out', @optvalue=N'false' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'sub', @optvalue=N'false' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'connect timeout', @optvalue=N'0' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'collation name', @optvalue=null GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'lazy schema validation', @optvalue=N'false' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'query timeout', @optvalue=N'0' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'use remote collation', @optvalue=N'true' GO EXEC master.dbo.sp_serveroption @server=N'ADSI', @optname=N'remote proc transaction promotion', @optvalue=N'true' GO Step 2 - Fetching ADSI dataTo get data, use OpenQuery against the Linked Server. In order to get only persons and no system accounts, you should filter out all users, which has no firstname (givenName) or lastname (sn): SELECT UserPrincipalName, DisplayName, sAMAccountName AS [SamAccountName], sn AS [LastName], givenName AS [FirstName], title AS [Title], Mail as [MailAddress], department AS [Department], l AS [Location], postalCode AS [PostCode], streetAddress AS [Street], st AS [State] FROM OpenQuery(ADSI, ' SELECT UserPrincipalName, DisplayName, sAMAccountName, sn, givenName, department, title, Mail, l, postalCode, streetAddress, st FROM ''LDAP://mydomain.de/DC=mydomain,DC=de'' WHERE objectClass = ''User'' AND objectCategory = ''Person'' AND sn=''*'' AND givenName = ''*'' ') In most cases you’re done with that … except your organisation has more the 900 users! Then you have to split the fetch in several requests, because SQL Server quits with an error, when trying to read more than 900 records via ADSI. Best option is, to filter the ADSI statement by something like ‘get all user starting with a to j’, when you are sure, that in this case less than 900 records will be given back and repeat the statement several times and glue the data together via a UNION statement: SELECT UserPrincipalName, DisplayName, sAMAccountName AS [SamAccountName], sn AS [LastName], givenName AS [FirstName], title AS [Title], Mail as [MailAddress], department AS [Department], l AS [Location], postalCode AS [PostCode], streetAddress AS [Street], st AS [State] FROM ( SELECT * FROM OpenQuery(ADSI, ' SELECT UserPrincipalName, DisplayName, sAMAccountName, sn, givenName, department, title, Mail, l, postalCode, streetAddress, st FROM ''LDAP://mydomain.de/DC=mydomain,DC=de'' WHERE objectClass = ''User'' AND objectCategory = ''Person'' AND sn=''*'' AND givenName = ''*'' AND sAMAccountName <= ''j'' ') UNION ALL SELECT * FROM OpenQuery(ADSI, ' SELECT [...same as above] FROM ''LDAP://mydomain.de/DC=mydomain,DC=de'' WHERE objectClass = ''User'' AND objectCategory = ''Person'' AND sn=''*'' AND givenName = ''*'' AND sAMAccountName > ''j'' AND sAMAccountName < ''p'' ') UNION ALL SELECT * FROM OpenQuery(ADSI, ' SELECT [...same as above] FROM ''LDAP://mydomain.de/DC=mydomain,DC=de'' WHERE objectClass = ''User'' AND objectCategory = ''Person'' AND sn=''*'' AND givenName = ''*'' AND sAMAccountName >= ''p'' ') ) AD When you store this as a VIEW, you can join it wherever you want on SQL Server: CREATE VIEW [dbo].[vADUsers] AS [...SQL code from above] GO Step 3 - SQL Server Database ProjectIf you work with a SQL Server Database Project, to have the complete structure of your database available in a version control system, you will get some reference errors on compiling and publishing your newly added SQL View vADUsers and on some objects, which rely on this View, because of following problems: Project doesn’t know the Linked Server ADSI The structure (fields) of the data source is unknown Declare the Linked ServerTo show the Project that there is a Linked Server called ADSI, just add following lines at the start of your view: sp_addlinkedserver 'ADSI' GO CREATE VIEW [dbo].[vADUsers] AS [...SQL code from above] This mimics the adding of a Linked Server, but will be ignored by SQL Server on publish, because you already have a Linked Server with this name. The project is happy with it. Declare the data structureWhen you use the SQL-View vADUsers in a Stored Procedure for example, this object won’t compile, because the project knows nothing about the fields of the ADSI data source. The SELECT in the view is not enough. You have to add an empty SELECT to the View vADUsers, just for the declaration of the fields and without returning any records and join this via UNION with the other statements: sp_addlinkedserver 'ADSI' GO CREATE VIEW [dbo].[vtADAllUsers] AS SELECT UserPrincipalName, DisplayName, sAMAccountName AS [SamAccountName], sn AS [LastName], givenName AS [FirstName], title AS [Title], Mail as [MailAddress], department AS [Department], l AS [Location], postalCode AS [PostCode], streetAddress AS [Street], st AS [State] FROM ( -- Fake SELECT to declare the structure of the view SELECT TOP 0 '' UserPrincipalName, '' DisplayName, '' sAMAccountName, '' sn, '' givenName, '' department, '' title, '' Mail, '' l, '' postalCode, '' streetAddress, '' st UNION ALL SELECT * FROM OpenQuery(ADSI, ' SELECT UserPrincipalName, DisplayName, sAMAccountName, sn, givenName, department, title, Mail, l, postalCode, streetAddress, st FROM ''LDAP://mydomain.de/DC=mydomain,DC=de'' WHERE objectClass = ''User'' AND objectCategory = ''Person'' AND sn=''*'' AND givenName = ''*'' AND sAMAccountName >= ''j'' ') UNION ALL SELECT * FROM OpenQuery(ADSI, ' SELECT [...same as above] FROM ''LDAP://mydomain.de/DC=mydomain,DC=de'' WHERE objectClass = ''User'' AND objectCategory = ''Person'' AND sn=''*'' AND givenName = ''*'' AND sAMAccountName < ''j'' AND sAMAccountName > ''p'' ') UNION ALL SELECT * FROM OpenQuery(ADSI, ' SELECT [...same as above] FROM ''LDAP://mydomain.de/DC=mydomain,DC=de'' WHERE objectClass = ''User'' AND objectCategory = ''Person'' AND sn=''*'' AND givenName = ''*'' AND sAMAccountName <= ''p'' ') ) AD Now, you can fetch data from Active Directory and store the code in a Database Project properly. HAPPY CODING :)","categories":[{"name":"SQL","slug":"SQL","permalink":"https://kiko.io/categories/SQL/"}],"tags":[{"name":"ADSI","slug":"ADSI","permalink":"https://kiko.io/tags/ADSI/"},{"name":"Visual Studio","slug":"Visual-Studio","permalink":"https://kiko.io/tags/Visual-Studio/"},{"name":"Database Project","slug":"Database-Project","permalink":"https://kiko.io/tags/Database-Project/"}]}],"categories":[{"name":"Football","slug":"Football","permalink":"https://kiko.io/categories/Football/"},{"name":"Tools","slug":"Tools","permalink":"https://kiko.io/categories/Tools/"},{"name":"Photo","slug":"Photo","permalink":"https://kiko.io/categories/Photo/"},{"name":"Misc","slug":"Misc","permalink":"https://kiko.io/categories/Misc/"},{"name":"UI/UX","slug":"UI-UX","permalink":"https://kiko.io/categories/UI-UX/"},{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/categories/JavaScript/"},{"name":".NET","slug":"NET","permalink":"https://kiko.io/categories/NET/"},{"name":"SQL","slug":"SQL","permalink":"https://kiko.io/categories/SQL/"}],"tags":[{"name":"SVWW","slug":"SVWW","permalink":"https://kiko.io/tags/SVWW/"},{"name":"2. Bundesliga","slug":"2-Bundesliga","permalink":"https://kiko.io/tags/2-Bundesliga/"},{"name":"Fediverse","slug":"Fediverse","permalink":"https://kiko.io/tags/Fediverse/"},{"name":"IndieWeb","slug":"IndieWeb","permalink":"https://kiko.io/tags/IndieWeb/"},{"name":"Identity","slug":"Identity","permalink":"https://kiko.io/tags/Identity/"},{"name":"Publishing","slug":"Publishing","permalink":"https://kiko.io/tags/Publishing/"},{"name":"Imaging","slug":"Imaging","permalink":"https://kiko.io/tags/Imaging/"},{"name":"Collection","slug":"Collection","permalink":"https://kiko.io/tags/Collection/"},{"name":"Trello","slug":"Trello","permalink":"https://kiko.io/tags/Trello/"},{"name":"Android","slug":"Android","permalink":"https://kiko.io/tags/Android/"},{"name":"CSS","slug":"CSS","permalink":"https://kiko.io/tags/CSS/"},{"name":"Theming","slug":"Theming","permalink":"https://kiko.io/tags/Theming/"},{"name":"Eintracht","slug":"Eintracht","permalink":"https://kiko.io/tags/Eintracht/"},{"name":"Windows","slug":"Windows","permalink":"https://kiko.io/tags/Windows/"},{"name":"Metadata","slug":"Metadata","permalink":"https://kiko.io/tags/Metadata/"},{"name":"Social Media","slug":"Social-Media","permalink":"https://kiko.io/tags/Social-Media/"},{"name":"Mastodon","slug":"Mastodon","permalink":"https://kiko.io/tags/Mastodon/"},{"name":"JavaScript","slug":"JavaScript","permalink":"https://kiko.io/tags/JavaScript/"},{"name":"VS Code","slug":"VS-Code","permalink":"https://kiko.io/tags/VS-Code/"},{"name":"Git/GitHub","slug":"Git-GitHub","permalink":"https://kiko.io/tags/Git-GitHub/"},{"name":"Concert","slug":"Concert","permalink":"https://kiko.io/tags/Concert/"},{"name":"Hexo","slug":"Hexo","permalink":"https://kiko.io/tags/Hexo/"},{"name":"GitHub","slug":"GitHub","permalink":"https://kiko.io/tags/GitHub/"},{"name":"Plugin","slug":"Plugin","permalink":"https://kiko.io/tags/Plugin/"},{"name":"Node.js","slug":"Node-js","permalink":"https://kiko.io/tags/Node-js/"},{"name":"Meta","slug":"Meta","permalink":"https://kiko.io/tags/Meta/"},{"name":"UI","slug":"UI","permalink":"https://kiko.io/tags/UI/"},{"name":"Usability","slug":"Usability","permalink":"https://kiko.io/tags/Usability/"},{"name":"jQuery","slug":"jQuery","permalink":"https://kiko.io/tags/jQuery/"},{"name":"Contributing","slug":"Contributing","permalink":"https://kiko.io/tags/Contributing/"},{"name":"Templating","slug":"Templating","permalink":"https://kiko.io/tags/Templating/"},{"name":"JSON-LD","slug":"JSON-LD","permalink":"https://kiko.io/tags/JSON-LD/"},{"name":"Lightroom","slug":"Lightroom","permalink":"https://kiko.io/tags/Lightroom/"},{"name":"Presets","slug":"Presets","permalink":"https://kiko.io/tags/Presets/"},{"name":"Search","slug":"Search","permalink":"https://kiko.io/tags/Search/"},{"name":"Audio","slug":"Audio","permalink":"https://kiko.io/tags/Audio/"},{"name":"Blogging","slug":"Blogging","permalink":"https://kiko.io/tags/Blogging/"},{"name":"Hosting","slug":"Hosting","permalink":"https://kiko.io/tags/Hosting/"},{"name":"SPA","slug":"SPA","permalink":"https://kiko.io/tags/SPA/"},{"name":"PWA","slug":"PWA","permalink":"https://kiko.io/tags/PWA/"},{"name":"Remote","slug":"Remote","permalink":"https://kiko.io/tags/Remote/"},{"name":"Workflow","slug":"Workflow","permalink":"https://kiko.io/tags/Workflow/"},{"name":"Bundling","slug":"Bundling","permalink":"https://kiko.io/tags/Bundling/"},{"name":"SVG","slug":"SVG","permalink":"https://kiko.io/tags/SVG/"},{"name":"Font","slug":"Font","permalink":"https://kiko.io/tags/Font/"},{"name":"Mail","slug":"Mail","permalink":"https://kiko.io/tags/Mail/"},{"name":"Office","slug":"Office","permalink":"https://kiko.io/tags/Office/"},{"name":"Visual Studio","slug":"Visual-Studio","permalink":"https://kiko.io/tags/Visual-Studio/"},{"name":"Logging","slug":"Logging","permalink":"https://kiko.io/tags/Logging/"},{"name":"C#","slug":"C","permalink":"https://kiko.io/tags/C/"},{"name":"Rant","slug":"Rant","permalink":"https://kiko.io/tags/Rant/"},{"name":"Browser","slug":"Browser","permalink":"https://kiko.io/tags/Browser/"},{"name":"Tutorial","slug":"Tutorial","permalink":"https://kiko.io/tags/Tutorial/"},{"name":"DOM","slug":"DOM","permalink":"https://kiko.io/tags/DOM/"},{"name":"ES6","slug":"ES6","permalink":"https://kiko.io/tags/ES6/"},{"name":"Share","slug":"Share","permalink":"https://kiko.io/tags/Share/"},{"name":"WebAPI","slug":"WebAPI","permalink":"https://kiko.io/tags/WebAPI/"},{"name":"Authentication","slug":"Authentication","permalink":"https://kiko.io/tags/Authentication/"},{"name":"Localization","slug":"Localization","permalink":"https://kiko.io/tags/Localization/"},{"name":"Debugging","slug":"Debugging","permalink":"https://kiko.io/tags/Debugging/"},{"name":"Events","slug":"Events","permalink":"https://kiko.io/tags/Events/"},{"name":"PowerShell","slug":"PowerShell","permalink":"https://kiko.io/tags/PowerShell/"},{"name":"MediaQuery","slug":"MediaQuery","permalink":"https://kiko.io/tags/MediaQuery/"},{"name":"Error","slug":"Error","permalink":"https://kiko.io/tags/Error/"},{"name":"Stylus","slug":"Stylus","permalink":"https://kiko.io/tags/Stylus/"},{"name":"Versioning","slug":"Versioning","permalink":"https://kiko.io/tags/Versioning/"},{"name":"T4","slug":"T4","permalink":"https://kiko.io/tags/T4/"},{"name":"Resource","slug":"Resource","permalink":"https://kiko.io/tags/Resource/"},{"name":"TFS/DevOps","slug":"TFS-DevOps","permalink":"https://kiko.io/tags/TFS-DevOps/"},{"name":"Dark Mode","slug":"Dark-Mode","permalink":"https://kiko.io/tags/Dark-Mode/"},{"name":"ADSI","slug":"ADSI","permalink":"https://kiko.io/tags/ADSI/"},{"name":"Database Project","slug":"Database-Project","permalink":"https://kiko.io/tags/Database-Project/"}]}
\ No newline at end of file
diff --git a/feed.json b/feed.json
index bc732b9525..1427064643 100644
--- a/feed.json
+++ b/feed.json
@@ -19,7 +19,7 @@
"image": "https://kiko.io/images/social-media/SVWW-vs-Braunschweig-2023-12-08.thumb.png",
"date_published": "2023-12-10T16:15:16.000Z",
"date_modified": null,
- "content_html": "\n
\n \n 1:3\n \n
\n \n\n
Our last home game of the year. The last two away games against Greuther Fürth and Holstein Kiel were unfortunately lost. One 2:0 and the other 3:2, so you could say that our good run had come to an end. But well … anyone who thought it would go on like this should have seen a doctor. After all, we’re the promoted team and we have to win against far stronger teams with far more Bundesliga experience. We can only ever catch them at a weak moment and hope that in the end it will be enough to stay in the 2. Bundesliga.
\n
Eintracht Braunschweig was one of the teams we thought had a chance, as they were in a relegation spot with just 8 points before the matchday (we have 21) and hadn’t won an away game for months.
\n\n\n
It’s already very cold in Wiesbaden at the beginning of December, especially as the weather was bringing in a lot of cold air from the north, so I left the scooter and took a cab to the Brita Arena. Freezing sucks and as I no longer have the heat of the youth, I bought long underwear for winter stadium visits. (Uhh, the old man wears long underpants). I was glad to have them though, because on the one hand I was there way too early and on the other hand you end up sitting for most of the 120 minutes that a game like this lasts, including waiting for kick-off and the interval. The hot cider and warm beef sausages only helped to combat the cold to a limited extent.
\n
Bärbel did better, as she arrived shortly before kick-off, like most of the 7,200 spectators. We chatted a bit about trivialities and hoped that our boys wouldn’t let the opportunity pass them by today.
\n\n
The Game
The coach seemed to have had something similar in mind, because as soon as the ball started rolling, our boys attacked their opponents and had their first 100% chance after just 20 seconds (!). A few minutes later, the ball was actually in the goal, but it was probably disallowed by the referee for offside or something similar. I don’t know but it didn’t matter as long as it continued at this pace. Braunschweig were hopelessly out of their depth and simply tried to prevent the inevitable … until the 18th minute … 1:0 - A fine header by our defender Aleksandar Vukotic :)
\n
However, the problem was that our team then let themselves go a little and only benefited from the opponent’s harmlessness until the break. As if the job was already done…
\n\n
\n \n
\n \n \n\n
Bärbel and I hoped that the coach in the dressing room would be able to shake the boys up again and they would play the second half with the same energy of the first 20 minutes. But what came was a bitter disappointment. Braunschweig’s coach seemed to have achieved exactly what we had hoped for. Our opponents came onto the pitch and started playing aggressive football. In the 48th minute, the score was 1:1 after a counterattack and only 8 minutes later, Braunschweig had turned the game around: 1:2.
\n
The lack of resistance and insecurity on our side was problematic. Hardly a ball got to where it needed to go, hardly a duel was won and even 5 substitutions unfortunately did nothing to change this. The boys were somehow beside themselves and so Braunschweig put the game to bed in the 76th minute: 1:3 :|
The stadium has rarely emptied as quickly as it did this time, but that wasn’t just due to the cold, but also the disappointment. Unfortunately, it wasn’t a Christmas present from the players to their fans in the last home game of 2023. The team have to play one last away game next week at St. Pauli before the break, but nobody expects a win against the strongest team in the 2nd division at the moment.
\n
No matter … we are in mid-table, which is more than we had hoped for at the start of the season. In 7 weeks time, we’ll be back in the Brita-Arena against Herta BSC and we’ll see better games again and stay in the league!
Our last home game of the year. The last two away games against Greuther Fürth and Holstein Kiel were unfortunately lost. One 2:0 and the other 3:2, so you could say that our good run had come to an end. But well … anyone who thought it would go on like this should have seen a doctor. After all, we’re the promoted team and we have to win against far stronger teams with far more Bundesliga experience. We can only ever catch them at a weak moment and hope that in the end it will be enough to stay in the 2. Bundesliga.
\n
Eintracht Braunschweig was one of the teams we thought had a chance, as they were in a relegation spot with just 8 points before the matchday (we have 21) and hadn’t won an away game for months.
\n\n\n
It’s already very cold in Wiesbaden at the beginning of December, especially as the weather was bringing in a lot of cold air from the north, so I left the scooter and took a cab to the Brita Arena. Freezing sucks and as I no longer have the heat of the youth, I bought long underwear for winter stadium visits. (Uhh, the old man wears long underpants). I was glad to have them though, because on the one hand I was there way too early and on the other hand you end up sitting for most of the 120 minutes that a game like this lasts, including waiting for kick-off and the interval. The hot cider and warm beef sausages only helped to combat the cold to a limited extent.
\n
Bärbel did better, as she arrived shortly before kick-off, like most of the 7,200 spectators. We chatted a bit about trivialities and hoped that our boys wouldn’t let the opportunity pass them by today.
\n\n
The Game
The coach seemed to have had something similar in mind, because as soon as the ball started rolling, our boys attacked their opponents and had their first 100% chance after just 20 seconds (!). A few minutes later, the ball was actually in the goal, but it was probably disallowed by the referee for offside or something similar. I don’t know but it didn’t matter as long as it continued at this pace. Braunschweig were hopelessly out of their depth and simply tried to prevent the inevitable … until the 18th minute … 1:0 - A fine header by our defender Aleksandar Vukotic :)
\n
However, the problem was that our team then let themselves go a little and only benefited from the opponent’s harmlessness until the break. As if the job was already done…
\n\n
\n \n
\n \n \n\n
Bärbel and I hoped that the coach in the dressing room would be able to shake the boys up again and they would play the second half with the same energy of the first 20 minutes. But what came was a bitter disappointment. Braunschweig’s coach seemed to have achieved exactly what we had hoped for. Our opponents came onto the pitch and started playing aggressive football. In the 48th minute, the score was 1:1 after a counterattack and only 8 minutes later, Braunschweig had turned the game around: 1:2.
\n
The lack of resistance and insecurity on our side was problematic. Hardly a ball got to where it needed to go, hardly a duel was won and even 5 substitutions unfortunately did nothing to change this. The boys were somehow beside themselves and so Braunschweig put the game to bed in the 76th minute: 1:3 :|
The stadium has rarely emptied as quickly as it did this time, but that wasn’t just due to the cold, but also the disappointment. Unfortunately, it wasn’t a Christmas present from the players to their fans in the last home game of 2023. The team have to play one last away game next week at St. Pauli before the break, but nobody expects a win against the strongest team in the 2nd division at the moment.
\n
No matter … we are in mid-table, which is more than we had hoped for at the start of the season. In 7 weeks time, we’ll be back in the Brita-Arena against Herta BSC and we’ll see better games again and stay in the league!
August in Germany is usually a safe bet when it comes to the weather, so my wife and I decided to relax for a few days on the Mecklenburg Lake Plateau … on a chartered boat. In general, you need a driving license for everything that moves in this country, but there are a few exceptions, such as in Brandenburg/Mecklenburg where even inexperienced tourists are allowed to steer a 12-metre houseboat/yacht across the many lakes after a short briefing.
\n
The weather was, let’s say, suboptimal this August, because a storm passed over us in the first three days and there was no chance of happy sailing around on the water. On the one hand, the swell and wind forces were extremely worrying for newbies and on the other, it’s simply no fun being on a boat in cold, heavy rain. We were stuck in Rheinsberg. On the one hand, the swell and wind forces were extremely worrying for newbies and on the other, it’s simply no fun being on a boat in cold, heavy rain.
\n\n\n
Not only because it felt like luxury camping without people but only water around, but also because it gave a pleasant feeling of freedom. Yes, we weren’t nearly alone on the lakes and getting a good spot in a marina for electricity and fresh water wasn’t always easy, but then just anchoring in any lake for the night and being woken up by birdsong and a gentle swell was magical.
\n
Below are a few pictures from this trip, some of which will be used again as hero images in this blog:
August in Germany is usually a safe bet when it comes to the weather, so my wife and I decided to relax for a few days on the Mecklenburg Lake Plateau … on a chartered boat. In general, you need a driving license for everything that moves in this country, but there are a few exceptions, such as in Brandenburg/Mecklenburg where even inexperienced tourists are allowed to steer a 12-metre houseboat/yacht across the many lakes after a short briefing.
\n
The weather was, let’s say, suboptimal this August, because a storm passed over us in the first three days and there was no chance of happy sailing around on the water. On the one hand, the swell and wind forces were extremely worrying for newbies and on the other, it’s simply no fun being on a boat in cold, heavy rain. We were stuck in Rheinsberg. On the one hand, the swell and wind forces were extremely worrying for newbies and on the other, it’s simply no fun being on a boat in cold, heavy rain.
\n\n\n
Not only because it felt like luxury camping without people but only water around, but also because it gave a pleasant feeling of freedom. Yes, we weren’t nearly alone on the lakes and getting a good spot in a marina for electricity and fresh water wasn’t always easy, but then just anchoring in any lake for the night and being woken up by birdsong and a gentle swell was magical.
\n
Below are a few pictures from this trip, some of which will be used again as hero images in this blog:
In September, my wife and I took a trip to the Museum of Technology in nearby Speyer. I don’t really know why I’ve never been there before, not even as a child, because the collection of cars and aeroplanes is quite unique in Germany. A dream for every boy, even if he’s not as young as he might think :)
\n
Built on the old site of an aircraft manufacturer, the 100-year-old hangars provide the perfect space for exhibiting large vehicles with wheels and wings. And exactly this can be found in Speyer: A large part for cars of all kinds and the outside area for aeroplanes and even ships. Even a spaceship, the Russian Space Shuttle alternative Buran can be seen there.
\n
Here are a few pictures of the car exhibition that I brought back from there and which will be used as post header images hero on kiko.io in the future …
In September, my wife and I took a trip to the Museum of Technology in nearby Speyer. I don’t really know why I’ve never been there before, not even as a child, because the collection of cars and aeroplanes is quite unique in Germany. A dream for every boy, even if he’s not as young as he might think :)
\n
Built on the old site of an aircraft manufacturer, the 100-year-old hangars provide the perfect space for exhibiting large vehicles with wheels and wings. And exactly this can be found in Speyer: A large part for cars of all kinds and the outside area for aeroplanes and even ships. Even a spaceship, the Russian Space Shuttle alternative Buran can be seen there.
\n
Here are a few pictures of the car exhibition that I brought back from there and which will be used as post header images hero on kiko.io in the future …
Travelling by scooter in Germany in the middle of November is no fun. Just like on Sunday. Light rain at just under 9 degrees Celsius and quite a bit of wind. Brrr… but what can I do, there are no car parks at the arena. Not at all today, because it’s against Kaiserslautern, which is only about 100 kilometres away from Wiesbaden and therefore a lot of visitors are expected. It’s a kind of derby.
\n
1. FC Kaiserslautern is one of the great traditional clubs in Germany, which has celebrated a total of 4 German Championships in its history, but has really lost momentum since its relegation to the 2. Bundesliga in 2006. Between 2018 and 2022, the team even only played in the 3rd division pf German football, but after their last year’s promotion, they are hoping to be promoted to the top division again soon. Recent results in the cup have shown that they have a strong team and it was to be feared that they would overrun Wehen Wiesbaden in their run, although their last league game was lost against Fürth.
\n
It was also rather uncomfortably cold in the stadium and I wondered once again how the young lads do it on the pitch in their shorts, especially the goalkeeper.
\n
But the fans created the right atmosphere. As expected, the entire guest stand was full. 12,100 spectators in total; a new season record. The SVWW fan block was also packed this time and the ultras had come up with a nice choreo with complete stand banners and red smoke (see photos).
\n\n\n
My nice seat neighbour, the older lady, was there again and we made friends this time. Her name is Bärbel and this time she was a bit more excited than normally. Well, the aim was to beat Kaiserslautern. Kaiserslautern! Every now and then she also started to grumble: about an opposing player or when things didn’t go fast enough forwards or when the ball was lost unnecessarily. Not nearly as blatant as the “gentlemen” behind us, who were swearing in one piece and indulging in football wisdom, but you could tell that the game was more important to her than others.
\n\n
The Game
In the first half, the game was rather well balanced, even if you had the feeling that our team was the only ones playing. There was no sign of Kaiserlautern’s attacking drive. Up until the 39th minute, we had one chance to score, but the visitors hadn’t even had a shot on our goal. Nothing, nada, niente. But then the ball was in our net: 0:1! A strange goal, because it didn’t look intentional. The ball just bounced at Ritter’s feet by chance and he simply took a shot. Goal. Damn…
\n\n
\n \n
\n \n \n\n
After the break, Kaiserslautern seemed to want to build on the goal. They were now playing football. And ours were probably angry at not having been rewarded for their efforts in the first half. The game became more lively, but we still had the better scenes … and after just 4 minutes, the visitors gave our Tijmen Goppel far too much time to aim and shoot just outside the penalty area and the ball was deflected into the goal by an opponent’s leg. 1:1!
\n
In the 65th minute, things got curious again: Goppel was fouled and awarded a free kick about 20 metres from goal. Robin Heußer took it and his shot somehow hit the chest of Ivan Prtajin in the penalty area, from where the ball went into the goal. 2:1 … Game turned round!
\n
The last half hour was really nerve-wracking. We continued to play better, but the pressure increased and the game became noticeably faster. Shortly before the end, substitute John Iredale ran alone with the ball across half the pitch and everyone held their breath, but just before the goal he tried to round the last defender and he was just able to scoop the ball away. G… damn it!
This was the fourth (!) game in a row that we have won and our position in the table looks correspondingly favourable again. Only three points behind the promotion places. Woohoo … but let’s not fool ourselves: There will be another losing streak in the future (as always in football) and 21 points are still not enough to keep us in the league, and that’s all that matters.
\n
Two away games are scheduled in the next three weeks, against Greuther Fürth and Holstein Kiel. On 8 December, we return to the Brita Arena against Eintracht Braunschweig, currently second last in the table. They should be beatable…
Travelling by scooter in Germany in the middle of November is no fun. Just like on Sunday. Light rain at just under 9 degrees Celsius and quite a bit of wind. Brrr… but what can I do, there are no car parks at the arena. Not at all today, because it’s against Kaiserslautern, which is only about 100 kilometres away from Wiesbaden and therefore a lot of visitors are expected. It’s a kind of derby.
\n
1. FC Kaiserslautern is one of the great traditional clubs in Germany, which has celebrated a total of 4 German Championships in its history, but has really lost momentum since its relegation to the 2. Bundesliga in 2006. Between 2018 and 2022, the team even only played in the 3rd division pf German football, but after their last year’s promotion, they are hoping to be promoted to the top division again soon. Recent results in the cup have shown that they have a strong team and it was to be feared that they would overrun Wehen Wiesbaden in their run, although their last league game was lost against Fürth.
\n
It was also rather uncomfortably cold in the stadium and I wondered once again how the young lads do it on the pitch in their shorts, especially the goalkeeper.
\n
But the fans created the right atmosphere. As expected, the entire guest stand was full. 12,100 spectators in total; a new season record. The SVWW fan block was also packed this time and the ultras had come up with a nice choreo with complete stand banners and red smoke (see photos).
\n\n\n
My nice seat neighbour, the older lady, was there again and we made friends this time. Her name is Bärbel and this time she was a bit more excited than normally. Well, the aim was to beat Kaiserslautern. Kaiserslautern! Every now and then she also started to grumble: about an opposing player or when things didn’t go fast enough forwards or when the ball was lost unnecessarily. Not nearly as blatant as the “gentlemen” behind us, who were swearing in one piece and indulging in football wisdom, but you could tell that the game was more important to her than others.
\n\n
The Game
In the first half, the game was rather well balanced, even if you had the feeling that our team was the only ones playing. There was no sign of Kaiserlautern’s attacking drive. Up until the 39th minute, we had one chance to score, but the visitors hadn’t even had a shot on our goal. Nothing, nada, niente. But then the ball was in our net: 0:1! A strange goal, because it didn’t look intentional. The ball just bounced at Ritter’s feet by chance and he simply took a shot. Goal. Damn…
\n\n
\n \n
\n \n \n\n
After the break, Kaiserslautern seemed to want to build on the goal. They were now playing football. And ours were probably angry at not having been rewarded for their efforts in the first half. The game became more lively, but we still had the better scenes … and after just 4 minutes, the visitors gave our Tijmen Goppel far too much time to aim and shoot just outside the penalty area and the ball was deflected into the goal by an opponent’s leg. 1:1!
\n
In the 65th minute, things got curious again: Goppel was fouled and awarded a free kick about 20 metres from goal. Robin Heußer took it and his shot somehow hit the chest of Ivan Prtajin in the penalty area, from where the ball went into the goal. 2:1 … Game turned round!
\n
The last half hour was really nerve-wracking. We continued to play better, but the pressure increased and the game became noticeably faster. Shortly before the end, substitute John Iredale ran alone with the ball across half the pitch and everyone held their breath, but just before the goal he tried to round the last defender and he was just able to scoop the ball away. G… damn it!
This was the fourth (!) game in a row that we have won and our position in the table looks correspondingly favourable again. Only three points behind the promotion places. Woohoo … but let’s not fool ourselves: There will be another losing streak in the future (as always in football) and 21 points are still not enough to keep us in the league, and that’s all that matters.
\n
Two away games are scheduled in the next three weeks, against Greuther Fürth and Holstein Kiel. On 8 December, we return to the Brita Arena against Eintracht Braunschweig, currently second last in the table. They should be beatable…
Football again. After my little excursion into the Europa Conference League, my team from SV Wehen Wiesbaden was called upon again on Sunday and with them the fans in the stadium. On the one hand because it was raining (and that is sometimes no fun in the front row) and on the other hand because the boys had won the last away game in Osnabrück and now had the opportunity to temporarily establish themselves in the midfield of the 2. Bundesliga. Another win was needed against an opponent from Rostock who, after a good start at the beginning of the season, had lost the last 6 games!
\n
Since I have no chance to park my car anywhere at the stadium, I have already planned before the season to always go there with the scooter, which is a pure pleasure in the summer. With 10 degrees and light rain it was diemal but rather suboptimal conditions. But anyway…
\n
I had expected that a lot of police, water cannon and other executive equipment would be on site, as it was already the case at the Schalke game, because the Hansa fans have a pretty bad reputation … But no. Not particularly many Rostockers have taken on the 700 kilometer journey and so the opposing stand was only half full and the police relaxed.
\n\n\n
The nice old lady to my left (I really have to ask her about her name) was also there again, after missing the last home game due to a wedding, as she told me. The silent one on my right also and also this time he didn’t speak a word, so I’m starting to wonder if he can talk at all. The howler monkeys behind me were of course also there again and they had in the course of the game again plenty of opportunity to insult the referee, opposing players or our coach in the worst way.
\n
This time there were only 8,600 spectators, which was probably also due to the weather. It was just cold and wet and I felt a bit sorry for the players, especially the goalkeepers. How do you keep warm when you’re standing on the field in shorts and not constantly running back and forth? A football mystery.
\n\n
The Game
In the first 30 minutes the game was quite even. You could tell that both teams wanted a win, but the goal attempts, especially from Rostock, were nothing more than puny. Ours did a little better, but the ball still always went past the goal. Both teams were also a bit aggressive, for example, Perea from Rostock annoyed our keeper Stritzel so much that he extended his elbow and Perea sank to the ground like a striving man and rolled back and forth theatrically. An undignified spectacle, which the fans acknowledged with shrill whistles … and the referee with a penalty!
\n
Stritzel has saved fantastic balls this season, but not a penalty yet … until then, as he parried Kinsombi’s weak shot to the side. Yesss…!
\n
Nothing much happened until half-time, but our guys were slowly gaining the upper hand, as the game was played almost exclusively in the opponent’s half at the beginning of the second half, with occasional counter-attacks by Rostock, but they were too erratic to have any effect. Our defence stood firm.
\n\n
\n \n
\n \n \n\n
The increasing control of the game also led to more chances to score, but either it went just over or Rostock keeper Markus Kolke (who played a total of 8 years in Wiesbaden until 2019) fished it out of the corners. The closer we got to the end of the game, the more nervous the fans (or just me and my seatmate) got. OMG, that thing has to go in, now!
\n
The redemption came in the 89th minute: After a standard, Kolke parried Fechner’s shot forward and Prtajin only had to stick his head out … 1:0!
It was a nerve-wracking, but in the end a happy game. We fans would like to see more of these games, especially if it ends in a win. I haven’t seen our boys lose since I started going to the stadium thsi season. We can keep it up :D
Football again. After my little excursion into the Europa Conference League, my team from SV Wehen Wiesbaden was called upon again on Sunday and with them the fans in the stadium. On the one hand because it was raining (and that is sometimes no fun in the front row) and on the other hand because the boys had won the last away game in Osnabrück and now had the opportunity to temporarily establish themselves in the midfield of the 2. Bundesliga. Another win was needed against an opponent from Rostock who, after a good start at the beginning of the season, had lost the last 6 games!
\n
Since I have no chance to park my car anywhere at the stadium, I have already planned before the season to always go there with the scooter, which is a pure pleasure in the summer. With 10 degrees and light rain it was diemal but rather suboptimal conditions. But anyway…
\n
I had expected that a lot of police, water cannon and other executive equipment would be on site, as it was already the case at the Schalke game, because the Hansa fans have a pretty bad reputation … But no. Not particularly many Rostockers have taken on the 700 kilometer journey and so the opposing stand was only half full and the police relaxed.
\n\n\n
The nice old lady to my left (I really have to ask her about her name) was also there again, after missing the last home game due to a wedding, as she told me. The silent one on my right also and also this time he didn’t speak a word, so I’m starting to wonder if he can talk at all. The howler monkeys behind me were of course also there again and they had in the course of the game again plenty of opportunity to insult the referee, opposing players or our coach in the worst way.
\n
This time there were only 8,600 spectators, which was probably also due to the weather. It was just cold and wet and I felt a bit sorry for the players, especially the goalkeepers. How do you keep warm when you’re standing on the field in shorts and not constantly running back and forth? A football mystery.
\n\n
The Game
In the first 30 minutes the game was quite even. You could tell that both teams wanted a win, but the goal attempts, especially from Rostock, were nothing more than puny. Ours did a little better, but the ball still always went past the goal. Both teams were also a bit aggressive, for example, Perea from Rostock annoyed our keeper Stritzel so much that he extended his elbow and Perea sank to the ground like a striving man and rolled back and forth theatrically. An undignified spectacle, which the fans acknowledged with shrill whistles … and the referee with a penalty!
\n
Stritzel has saved fantastic balls this season, but not a penalty yet … until then, as he parried Kinsombi’s weak shot to the side. Yesss…!
\n
Nothing much happened until half-time, but our guys were slowly gaining the upper hand, as the game was played almost exclusively in the opponent’s half at the beginning of the second half, with occasional counter-attacks by Rostock, but they were too erratic to have any effect. Our defence stood firm.
\n\n
\n \n
\n \n \n\n
The increasing control of the game also led to more chances to score, but either it went just over or Rostock keeper Markus Kolke (who played a total of 8 years in Wiesbaden until 2019) fished it out of the corners. The closer we got to the end of the game, the more nervous the fans (or just me and my seatmate) got. OMG, that thing has to go in, now!
\n
The redemption came in the 89th minute: After a standard, Kolke parried Fechner’s shot forward and Prtajin only had to stick his head out … 1:0!
It was a nerve-wracking, but in the end a happy game. We fans would like to see more of these games, especially if it ends in a win. I haven’t seen our boys lose since I started going to the stadium thsi season. We can keep it up :D
My newly awakened love for German soccer reached a small peak last Thursday when my colleague Uli asked me, if I would like to go to the evening match of his favorite club Eintracht Frankfurt against HJK Helsinki as part of the preliminary round of the Europa Conference League. He had come by a happy coincidence to two tickets. Of course! Let’s go!
\n
Now I’m not an Eintracht fan, by any means. In the Rhine-Main area, the Frankfurt club is either hated or loved by the people, because its fan base is a bit more aggressive, i.e. they are disreputable and with them the club. But I’ve never been to the Waldstadion (as it used to be called) or the “Deutsche Bank Park” (as it’s called if you put money on the table), one of the big soccer arenas in Germany, where international matches and other big events are also held regularly.
\n\n\n
Getting to the stadium was an ordeal, because we had left our cars at the office and taken the cab, but the 55,500 spectators all seemed to want to park right in front of the arena so the traffic jam was immense. The last kilometer through the forest to the stadium and the Jürgen Grabowski grandstand to our seats were tough, at least for Uli, because his knee was hurting as usual. But the anticipation, a beer and an Äppler made up for everything.
\n
What I found amazing, as a Waldstadion newbie, was that we entered the building at ground level, but came out in row 30. The whole thing thus reminded of a nicely designed giant pit. The lighting of this massive area was perfect and the volume of the fans was loud, but not deafening. An architectural masterpiece. The wall of fans in the home curve with the many oversized flags that were waved throughout was impressive, even if I wondered the whole time, how the fans behind these giant flags would have noticed anything of the game for even a minute. But I think it was not about free view of the people there at all. Only about the atmosphere. I was in any case happy about my seat in row 9 pretty directly on the center line of the field and had to ask the people in the row of seats in front of me once in a while to sit down again so that I could see the game comfortably.
\n\n
… vs … (cute)
\n\n\n
The Game
The first 10 minutes of the game were unspectacular. They felt each other out and the Finns played diligently and should have been in the lead after 5 minutes, but got in the 9th minute after some guesswork from the referee with VAR support a penalty against them.
\n\n
\n \n
\n \n \n\n \n \n \n\n
After this 1:0 for Eintracht, the Frankfurt boys whirled themselves into a kind of frenzy of play. More and more fluidly and casually it went in the direction of the Finnish goal and Helsinki could only with effort hold against it. So it was after 30 minutes already 3:0 and at halftime 4:0. The goal to make it 3-0 was a really spectacular one: Omar Marmoush gets the ball in the penalty area and takes a shot, but the ball bounces off an opponent’s legs and comes back to him. He pulls off again and again the ball bounces off the legs of another player back to him. His third attempt was then a fine shot with the outside of his foot past all the players into the goal.
\n
In the break, the coach of the Finns seems to have been a little louder, because his guys played a little stronger and more determined, only to get another one in the 55th minute.
\n
With 10 minutes to go, things got emotional in the stadium: the coach substituted Timothy Chandler, a Frankfurt veteran who has been with the club for almost 10 years and his first appearance this season. The tens of thousands of fans were completely out of their minds, screaming “Tiiiimmmyyy” as soon as he got to the ball. It was crazy. How must it feel to be showered with so much fan love! Things almost got out of hand when Chandler sprinted forward on the right flank in the 89th minute and made a wonderful pass into the middle and Dina Ebimbe just had to slot it in for 6:0.
It was really an experience to see this game in this arena and even if I would have enjoyed Helsinki sending Frankfurt home 4:0 (my funny tip to Uli before the game), it was nice that he could be happy about the victory.
\n",
+ "content_html": "\n
\n \n 6:0\n \n
\n \n\n
My newly awakened love for German soccer reached a small peak last Thursday when my colleague Uli asked me, if I would like to go to the evening match of his favorite club Eintracht Frankfurt against HJK Helsinki as part of the preliminary round of the Europa Conference League. He had come by a happy coincidence to two tickets. Of course! Let’s go!
\n
Now I’m not an Eintracht fan, by any means. In the Rhine-Main area, the Frankfurt club is either hated or loved by the people, because its fan base is a bit more aggressive, i.e. they are disreputable and with them the club. But I’ve never been to the Waldstadion (as it used to be called) or the “Deutsche Bank Park” (as it’s called if you put money on the table), one of the big soccer arenas in Germany, where international matches and other big events are also held regularly.
\n\n\n
Getting to the stadium was an ordeal, because we had left our cars at the office and taken the cab, but the 55,500 spectators all seemed to want to park right in front of the arena so the traffic jam was immense. The last kilometer through the forest to the stadium and the Jürgen Grabowski grandstand to our seats were tough, at least for Uli, because his knee was hurting as usual. But the anticipation, a beer and an Äppler made up for everything.
\n
What I found amazing, as a Waldstadion newbie, was that we entered the building at ground level, but came out in row 30. The whole thing thus reminded of a nicely designed giant pit. The lighting of this massive area was perfect and the volume of the fans was loud, but not deafening. An architectural masterpiece. The wall of fans in the home curve with the many oversized flags that were waved throughout was impressive, even if I wondered the whole time, how the fans behind these giant flags would have noticed anything of the game for even a minute. But I think it was not about free view of the people there at all. Only about the atmosphere. I was in any case happy about my seat in row 9 pretty directly on the center line of the field and had to ask the people in the row of seats in front of me once in a while to sit down again so that I could see the game comfortably.
\n\n
… vs … (cute)
\n\n\n
The Game
The first 10 minutes of the game were unspectacular. They felt each other out and the Finns played diligently and should have been in the lead after 5 minutes, but got in the 9th minute after some guesswork from the referee with VAR support a penalty against them.
\n\n
\n \n
\n \n \n\n \n \n \n\n
After this 1:0 for Eintracht, the Frankfurt boys whirled themselves into a kind of frenzy of play. More and more fluidly and casually it went in the direction of the Finnish goal and Helsinki could only with effort hold against it. So it was after 30 minutes already 3:0 and at halftime 4:0. The goal to make it 3-0 was a really spectacular one: Omar Marmoush gets the ball in the penalty area and takes a shot, but the ball bounces off an opponent’s legs and comes back to him. He pulls off again and again the ball bounces off the legs of another player back to him. His third attempt was then a fine shot with the outside of his foot past all the players into the goal.
\n
In the break, the coach of the Finns seems to have been a little louder, because his guys played a little stronger and more determined, only to get another one in the 55th minute.
\n
With 10 minutes to go, things got emotional in the stadium: the coach substituted Timothy Chandler, a Frankfurt veteran who has been with the club for almost 10 years and his first appearance this season. The tens of thousands of fans were completely out of their minds, screaming “Tiiiimmmyyy” as soon as he got to the ball. It was crazy. How must it feel to be showered with so much fan love! Things almost got out of hand when Chandler sprinted forward on the right flank in the 89th minute and made a wonderful pass into the middle and Dina Ebimbe just had to slot it in for 6:0.
It was really an experience to see this game in this arena and even if I would have enjoyed Helsinki sending Frankfurt home 4:0 (my funny tip to Uli before the game), it was nice that he could be happy about the victory.
Since my youth I am a (small) fan of the Hamburger Sportverein (HSV). Magath, Hrubesch, Keegan were my sporting heroes back then. This was certainly also related to the fact that the club was one of the best in Europe in the early 80s. Two championships, European Cup victory and a strong team left an impression on me. As the last founding member of the 1. Bundesliga, however, it was always in danger of relegation in the last 10 years and then really relegated to the 2. Bundesliga in 2018 after 55 years. What followed were sad attempts to climb back up. Always so close that the saying prevailed: “How can you tell that it’s springtime in Germany? The trees are sprouting and HSV is fucking up the promotion!”
\n
In my first post in this series, I described how my hometown club from Wiesbaden made it into the 2. Bundeliga and this particular home game this weekend has a special appeal for me, of course. The favorite club from my youth against the one from my present. My Wehen Wiesbaden against my HSV.
\n\n\n
Unfortunately I missed the game two weeks ago against Elversberg, because I had already made a promise to go to Mannheim to the Bundesgartenschau, but maybe it was a good thing, because my team was in a bad mood and lost 0:2. Just like the last away game against Hannover (2:0) and the cup game against RB Leipzig at home in between (2:3), where I actually wanted to go, but due to a small error in understanding did not get into the stadium. So it was time to go back to the arena, because with me SVWW always had something to cheer about, even if it was just a draw that felt like a win.
\n
I also have a few HSV fans in my circle of friends and so I made use of my right of first refusal of 4 more tickets for this game. I sat on my permanent seat in the front row and my friends at a few rows behind me. The elderly lady on my left was not present this time, but a nice man who had taken her place was. The silent one on my right did not come at all and his seat thus remained empty.
\n\n
The Game
HSV is actually in shape this year to make it back to the Bundesliga. They have already dropped a few points, but have been on the promotion places from the beginning. This year it should be something and accordingly dominant have the Hamburg the game against once again deep standing Wiesbadener also started. Very sure of the ball and winning almost every duel, they were unable to capitalize on this in the first half. They brought the ball very close to the goal, but not into it, and you could tell that the guests were getting a little more annoyed as time went on.
\n
This was expressed shortly before the half-time break also in a few unsportsmanlike conduct. Dropping and claiming foul play is simply stupid and only provokes catcalls. The one or other time is overall bad referee but also fell for the trick or has made other nonsensical decisions, which caused the “right” fans around me to nastiest insults.
\n\n
\n \n
\n \n \n\n
After the break, the game continued just as it had stopped a quarter of an hour earlier: HSV played beautifully, safely but ineffectively. They even scored a goal, but it was disallowed for offside. The SVWW was defending with all men in their own half, sometimes desperately, and only rarely managed to counterattack.
\n
9 minutes before the end, the Wiesbadener were then once in front of the opposing goal and from a rather harmless header and a weak defense of the goalkeeper became a goal! 1:0! Unbelievable!
\n
The remaining 9 minutes plus 6 minutes of injury time can be called spectacular: HSV ran frantically and tried everything to put the f***ing ball in the goal and in the 87th minute the ball was actually in our net … 1:1! But that was not enough for the guests. They kept increasing the pressure to win this game. Partly too hectic and headless, but we had real trouble to fend them off. However, one of these defensive actions in the penalty area led to a penalty kick for HSV in the 97th minute. Damn! No! … but the penalty taker slammed the thing against the crossbar! YESSS!
Another one of those draws that feels like a win. It’s a pity that it wasn’t enough for a win, but you have to admit that HSV was really a class above. They’re really playing for promotion and we’re playing to stay in the league. What gives me courage is that the Wiesbaden team fights so passionately and never gives up. Every opponent has a very hard time with this defense and that’s a good approach to still play in the 2nd Bundesliga next year.
Since my youth I am a (small) fan of the Hamburger Sportverein (HSV). Magath, Hrubesch, Keegan were my sporting heroes back then. This was certainly also related to the fact that the club was one of the best in Europe in the early 80s. Two championships, European Cup victory and a strong team left an impression on me. As the last founding member of the 1. Bundesliga, however, it was always in danger of relegation in the last 10 years and then really relegated to the 2. Bundesliga in 2018 after 55 years. What followed were sad attempts to climb back up. Always so close that the saying prevailed: “How can you tell that it’s springtime in Germany? The trees are sprouting and HSV is fucking up the promotion!”
\n
In my first post in this series, I described how my hometown club from Wiesbaden made it into the 2. Bundeliga and this particular home game this weekend has a special appeal for me, of course. The favorite club from my youth against the one from my present. My Wehen Wiesbaden against my HSV.
\n\n\n
Unfortunately I missed the game two weeks ago against Elversberg, because I had already made a promise to go to Mannheim to the Bundesgartenschau, but maybe it was a good thing, because my team was in a bad mood and lost 0:2. Just like the last away game against Hannover (2:0) and the cup game against RB Leipzig at home in between (2:3), where I actually wanted to go, but due to a small error in understanding did not get into the stadium. So it was time to go back to the arena, because with me SVWW always had something to cheer about, even if it was just a draw that felt like a win.
\n
I also have a few HSV fans in my circle of friends and so I made use of my right of first refusal of 4 more tickets for this game. I sat on my permanent seat in the front row and my friends at a few rows behind me. The elderly lady on my left was not present this time, but a nice man who had taken her place was. The silent one on my right did not come at all and his seat thus remained empty.
\n\n
The Game
HSV is actually in shape this year to make it back to the Bundesliga. They have already dropped a few points, but have been on the promotion places from the beginning. This year it should be something and accordingly dominant have the Hamburg the game against once again deep standing Wiesbadener also started. Very sure of the ball and winning almost every duel, they were unable to capitalize on this in the first half. They brought the ball very close to the goal, but not into it, and you could tell that the guests were getting a little more annoyed as time went on.
\n
This was expressed shortly before the half-time break also in a few unsportsmanlike conduct. Dropping and claiming foul play is simply stupid and only provokes catcalls. The one or other time is overall bad referee but also fell for the trick or has made other nonsensical decisions, which caused the “right” fans around me to nastiest insults.
\n\n
\n \n
\n \n \n\n
After the break, the game continued just as it had stopped a quarter of an hour earlier: HSV played beautifully, safely but ineffectively. They even scored a goal, but it was disallowed for offside. The SVWW was defending with all men in their own half, sometimes desperately, and only rarely managed to counterattack.
\n
9 minutes before the end, the Wiesbadener were then once in front of the opposing goal and from a rather harmless header and a weak defense of the goalkeeper became a goal! 1:0! Unbelievable!
\n
The remaining 9 minutes plus 6 minutes of injury time can be called spectacular: HSV ran frantically and tried everything to put the f***ing ball in the goal and in the 87th minute the ball was actually in our net … 1:1! But that was not enough for the guests. They kept increasing the pressure to win this game. Partly too hectic and headless, but we had real trouble to fend them off. However, one of these defensive actions in the penalty area led to a penalty kick for HSV in the 97th minute. Damn! No! … but the penalty taker slammed the thing against the crossbar! YESSS!
Another one of those draws that feels like a win. It’s a pity that it wasn’t enough for a win, but you have to admit that HSV was really a class above. They’re really playing for promotion and we’re playing to stay in the league. What gives me courage is that the Wiesbaden team fights so passionately and never gives up. Every opponent has a very hard time with this defense and that’s a good approach to still play in the 2nd Bundesliga next year.
Summer is over and I’m spending some of my free time post-processing in Lightroom the many photos I’ve taken over the past few months. As a casual photographer, I mostly use my vacation and weekend trips to take my Nikon for a walk and let its chip card glow. My better half is constantly wondering why I stopped there or here again, even if she likes the results afterwards. The point is that I am a color-and-shape junkie. Of course I also do landscapes and portraits, but the details of things have done it to me the most. Colors & Shapes …
\n
In July I was for a few days with friends on a finca in the south of Mallorca and of course I used every opportunity to stick my lens everywhere where there were interesting details to be expected. I don’t know if it had anything to do with the heat, but most of the motifs have a very earthy touch. I probably wanted to stay somewhere underground all the time, because 40 degrees Celsius in the shade was then also a bit too much for me.
\n\n\n
Usually the best photos end up unnoticed as pool photos here on my blog until I use one as a header image, but now I make a separate post out of each set before I publish the individual photos on other sites like 500px or Pixelfed.
Summer is over and I’m spending some of my free time post-processing in Lightroom the many photos I’ve taken over the past few months. As a casual photographer, I mostly use my vacation and weekend trips to take my Nikon for a walk and let its chip card glow. My better half is constantly wondering why I stopped there or here again, even if she likes the results afterwards. The point is that I am a color-and-shape junkie. Of course I also do landscapes and portraits, but the details of things have done it to me the most. Colors & Shapes …
\n
In July I was for a few days with friends on a finca in the south of Mallorca and of course I used every opportunity to stick my lens everywhere where there were interesting details to be expected. I don’t know if it had anything to do with the heat, but most of the motifs have a very earthy touch. I probably wanted to stay somewhere underground all the time, because 40 degrees Celsius in the shade was then also a bit too much for me.
\n\n\n
Usually the best photos end up unnoticed as pool photos here on my blog until I use one as a header image, but now I make a separate post out of each set before I publish the individual photos on other sites like 500px or Pixelfed.
Since my youth with I hardrock/metal fan, but from good music I let myself convince, even if it does not fit into this scheme. This is what happened with the German medieval/folk band Versengold from Bremen, to whose concert in Bochum my better half dragged me one day. And what can I say … the guys are so much fun with their easy-going manner, their good, funny and sometimes profound German lyrics and their shanty-like music, from which the North German sailor tradition can be clearly heard.
\n\n\n
In the meantime we were on two more concerts in Frankfurt, then in Marburg (Open-Air) and in Mainz on the “Night of the Ballads”. Most recently we saw them two weeks ago at the MPS (“Mittelalterlich Phantasie Spectaculum”, a medieval festival) in Speyer, where they played open-air with other bands from the “industry”. On the one hand, I think it’s great that the guys, even if they now fill large halls in Germany, have not forgotten their origins, and on the other hand, I now had the opportunity to get very close to the stage with my camera.
\n
Here are the results:
\n\n
\n \n
\n \n \n\n
Love you guys…!
\n",
+ "content_html": "
Since my youth with I hardrock/metal fan, but from good music I let myself convince, even if it does not fit into this scheme. This is what happened with the German medieval/folk band Versengold from Bremen, to whose concert in Bochum my better half dragged me one day. And what can I say … the guys are so much fun with their easy-going manner, their good, funny and sometimes profound German lyrics and their shanty-like music, from which the North German sailor tradition can be clearly heard.
\n\n\n
In the meantime we were on two more concerts in Frankfurt, then in Marburg (Open-Air) and in Mainz on the “Night of the Ballads”. Most recently we saw them two weeks ago at the MPS (“Mittelalterlich Phantasie Spectaculum”, a medieval festival) in Speyer, where they played open-air with other bands from the “industry”. On the one hand, I think it’s great that the guys, even if they now fill large halls in Germany, have not forgotten their origins, and on the other hand, I now had the opportunity to get very close to the stage with my camera.
On this game, some in my circle of friends have feverishly awaited, especially my neighbor and friend, who for years is an ardent fan of one of the traditional clubs Schalke 04. We got six additional tickets for the game in time and with a crowd of 11,003 fans, this was also urgently necessary. The stadium (12,566 standing and seated) was full to the roof. Only one of three blocks of the guests was empty. Some Schalke fans seem to have expected nothing from the game in Wiesbaden. No wonder after table position 15 after the last match day.
\n
Since season ticket holders get into the stadium a little faster and my friends sat a little scattered in different blocks, I lost sight of them at some point, but that was not tragic, because they had fun.
\n
To my delight, the booth operators, who I had to criticize last time, actually did a better job today. Two cash registers: one for cash and another for card/smartphone/watch payers. The sale of beer and bratwurst went much more quickly, only I had to stand in line a bit at the fan shop, because I was not yet recognizable as a fan: it had to get a cap and a jersey, of course, finally.
\n\n\n
My fellow fans in row 1 were rather quiet today, despite the good weather and the atmosphere around. In my back there was now and then a comment like “Now RUN!” or “Go ahead. The goal is in THIS direction!”, but I could understand them, because our team hardly dared to cross the center line in the first half. I still don’t know the name of the nice lady next to me, but I will. She suffered today like many rather quietly.
\n\n
The Game
In the first half we were, as they say in the jargon “compact in defense”, which means nothing else, that no one dared to run forward with the ball. Except for once, when after about 20 minutes Kianz Froese, our Canadian striker, ran into the box on the left side at full gallop, but unfortunately missed the goal by a hair’s breadth. Damn! Our team let the opponent play and again straddled every ball off their feet.
\n
It wasn’t, however, that Schalke dominated the game. We carelessly let them just do it and the fans had their fun when they shot the ball into the clouds again after a lot of roaring and stomping. Funny.
\n
At the beginning of the second half, this continued seamlessly, even if you noticed that the coach in the cabin must have yelled at one or the other and these were now somewhat more active … and suddenly the ball was in our net … 0:1! What? How? A goal in the 54. minute for Schalke out of (almost) nothing! I have to watch that again on TV … :|
\n\n
\n \n
\n \n \n\n
After our guys had digested the shock, after 10 minutes or so, they began … yes indeed … to play football! Also because Kauczinski had replaced a few snoozers. In the last 20 minutes it went with steam on the opponents goal, but we ran slightly out of time. In the 90th minute, 6 minutes of injury time were displayed and it was almost hectic … kick and rush. With success! In the 95th minute it was 1:1, thanks to the leg of Reinthaler, from which bounced a defensive attempt, and in the 97th almost 2:1 … huuuuh, damn close.
\n
Before the game I had bet on a 1:1 and was unfortunately/fortunately right. It felt like a victory.
Today we are in 6th place in the table and I don’t really understand why the team was so anxious. Yes, it’s Schalke 04, but they’re further down so far and we certainly don’t need to hide!
On this game, some in my circle of friends have feverishly awaited, especially my neighbor and friend, who for years is an ardent fan of one of the traditional clubs Schalke 04. We got six additional tickets for the game in time and with a crowd of 11,003 fans, this was also urgently necessary. The stadium (12,566 standing and seated) was full to the roof. Only one of three blocks of the guests was empty. Some Schalke fans seem to have expected nothing from the game in Wiesbaden. No wonder after table position 15 after the last match day.
\n
Since season ticket holders get into the stadium a little faster and my friends sat a little scattered in different blocks, I lost sight of them at some point, but that was not tragic, because they had fun.
\n
To my delight, the booth operators, who I had to criticize last time, actually did a better job today. Two cash registers: one for cash and another for card/smartphone/watch payers. The sale of beer and bratwurst went much more quickly, only I had to stand in line a bit at the fan shop, because I was not yet recognizable as a fan: it had to get a cap and a jersey, of course, finally.
\n\n\n
My fellow fans in row 1 were rather quiet today, despite the good weather and the atmosphere around. In my back there was now and then a comment like “Now RUN!” or “Go ahead. The goal is in THIS direction!”, but I could understand them, because our team hardly dared to cross the center line in the first half. I still don’t know the name of the nice lady next to me, but I will. She suffered today like many rather quietly.
\n\n
The Game
In the first half we were, as they say in the jargon “compact in defense”, which means nothing else, that no one dared to run forward with the ball. Except for once, when after about 20 minutes Kianz Froese, our Canadian striker, ran into the box on the left side at full gallop, but unfortunately missed the goal by a hair’s breadth. Damn! Our team let the opponent play and again straddled every ball off their feet.
\n
It wasn’t, however, that Schalke dominated the game. We carelessly let them just do it and the fans had their fun when they shot the ball into the clouds again after a lot of roaring and stomping. Funny.
\n
At the beginning of the second half, this continued seamlessly, even if you noticed that the coach in the cabin must have yelled at one or the other and these were now somewhat more active … and suddenly the ball was in our net … 0:1! What? How? A goal in the 54. minute for Schalke out of (almost) nothing! I have to watch that again on TV … :|
\n\n
\n \n
\n \n \n\n
After our guys had digested the shock, after 10 minutes or so, they began … yes indeed … to play football! Also because Kauczinski had replaced a few snoozers. In the last 20 minutes it went with steam on the opponents goal, but we ran slightly out of time. In the 90th minute, 6 minutes of injury time were displayed and it was almost hectic … kick and rush. With success! In the 95th minute it was 1:1, thanks to the leg of Reinthaler, from which bounced a defensive attempt, and in the 97th almost 2:1 … huuuuh, damn close.
\n
Before the game I had bet on a 1:1 and was unfortunately/fortunately right. It felt like a victory.
Today we are in 6th place in the table and I don’t really understand why the team was so anxious. Yes, it’s Schalke 04, but they’re further down so far and we certainly don’t need to hide!
Displaying a few more images than usual in a post is always a bit tricky, because you have to make sure they don’t get too big and drown out the text. But they should not be too small either and the arrangement is also important to consider.
\n
For this purpose I have so far used my Image Slider Tag Plugin, but with this you only ever see one of the images and have to scroll through the rest horizontally. A medium sized overview, best in the so called masonry format, where images are automatically assembled based on their size on a limited area, would be much better for some cases. There are a variety of CSS or JavaScript solutions out there on the net, but the most suitable for me was Macy.js … and how I integrated it into my Hexo environment is what I want to describe here.
\n\n\n
Like (Tiny Slider), Macy.js is also based on JavaScript, as the name already expresses. The setup in HTML is very simple: a certain number of wrappers are arranged in a container, each of which contains an image:
It does not matter whether the images are the same size or whether they are in portrait or landscape format. Macy.js then takes care of the sensible arrangement of the images in the container. All that is missing now is the call to the script:
Displaying a few more images than usual in a post is always a bit tricky, because you have to make sure they don’t get too big and drown out the text. But they should not be too small either and the arrangement is also important to consider.
\n
For this purpose I have so far used my Image Slider Tag Plugin, but with this you only ever see one of the images and have to scroll through the rest horizontally. A medium sized overview, best in the so called masonry format, where images are automatically assembled based on their size on a limited area, would be much better for some cases. There are a variety of CSS or JavaScript solutions out there on the net, but the most suitable for me was Macy.js … and how I integrated it into my Hexo environment is what I want to describe here.
\n\n\n
Like (Tiny Slider), Macy.js is also based on JavaScript, as the name already expresses. The setup in HTML is very simple: a certain number of wrappers are arranged in a container, each of which contains an image:
It does not matter whether the images are the same size or whether they are in portrait or landscape format. Macy.js then takes care of the sensible arrangement of the images in the container. All that is missing now is the call to the script:
+ data-id="clq2grrbm0037nwpx7dh79vv3" data-title="Experimenting with the font LEXEND" />
diff --git a/meta.json b/meta.json
index eaeec23567..fbacd3db4a 100644
--- a/meta.json
+++ b/meta.json
@@ -1 +1 @@
-{"tags":["2. Bundesliga","ADSI","Android","Audio","Authentication","Blogging","Browser","Bundling","C#","CSS","Collection","Concert","Contributing","DOM","Dark Mode","Database Project","Debugging","ES6","Eintracht","Error","Events","Fediverse","Font","Git/GitHub","GitHub","Hexo","Hosting","Identity","Imaging","IndieWeb","JSON-LD","JavaScript","Lightroom","Localization","Logging","Mail","Mastodon","MediaQuery","Meta","Metadata","Node.js","Office","PWA","Plugin","PowerShell","Presets","Publishing","Rant","Remote","Resource","SPA","SVG","SVWW","Search","Share","Social Media","Stylus","T4","TFS/DevOps","Templating","Theming","Trello","Tutorial","UI","Usability","VS Code","Versioning","Visual Studio","WebAPI","Windows","Workflow","jQuery"],"categories":[".NET","Football","JavaScript","Misc","Photo","SQL","Tools","UI/UX"],"posts":[{"title":"A New Blog: Blogging and Synching en route","url":"https://kiko.io/post/A-New-Blog-Blogging-and-Synching-en-route/","date":"2019-09-30T22:00:00.000Z"},{"title":"A New Blog: Customizing Hexo","url":"https://kiko.io/post/A-New-Blog-Customizing-Hexo/","date":"2019-09-25T10:00:00.000Z"},{"title":"A New Blog: VS Code, Hexo and GitHub Pages","url":"https://kiko.io/post/A-New-Blog-VS-Code-Hexo-and-GitHub-Pages/","date":"2019-09-23T22:00:00.000Z"},{"title":"Better Input Change Event","url":"https://kiko.io/post/Better-Input-Change-Event/","date":"2019-11-26T15:51:17.000Z"},{"title":"Hexo and the Dark Mode ... revised","url":"https://kiko.io/post/Hexo-and-the-Dark-Mode-revised/","date":"2019-10-26T12:08:05.000Z"},{"title":"Hexo and the Dark Mode","url":"https://kiko.io/post/Hexo-and-the-Dark-Mode/","date":"2019-10-23T13:28:04.000Z"},{"title":"How-To: Visual Studio Database Project and ADSI","url":"https://kiko.io/post/How-To-Visual-Studio-Database-Project-and-ADSI/","date":"2019-09-17T10:00:01.000Z"},{"title":"Add website to Trello card the better way","url":"https://kiko.io/post/Add-website-to-Trello-card-the-better-way/","date":"2020-09-07T13:17:21.000Z"},{"title":"404 Page in Hexo for GitHub Pages","url":"https://kiko.io/post/404-Page-in-Hexo-for-GitHub-Pages/","date":"2020-09-23T12:28:50.000Z"},{"title":"Automatic Header Images in Hexo","url":"https://kiko.io/post/Automatic-Header-Images-in-Hexo/","date":"2020-06-22T15:49:16.000Z"},{"title":"Change CSS class when element scrolls into viewport","url":"https://kiko.io/post/Change-CSS-class-when-element-scrolls-into-viewport/","date":"2020-07-13T16:24:39.000Z"},{"title":"Device Class Detection in JavaScript","url":"https://kiko.io/post/Device-Class-Detection-in-JavaScript/","date":"2020-09-28T13:27:17.000Z"},{"title":"Discoveries #1","url":"https://kiko.io/post/Discoveries-1/","date":"2020-07-12T07:55:10.000Z"},{"title":"Discoveries #2","url":"https://kiko.io/post/Discoveries-2/","date":"2020-09-07T15:54:50.000Z"},{"title":"Discoveries #3 - Tutorials","url":"https://kiko.io/post/Discoveries-3-Tutorials/","date":"2020-09-29T10:02:10.000Z"},{"title":"Discoveries #4","url":"https://kiko.io/post/Discoveries-4/","date":"2020-10-10T13:27:47.000Z"},{"title":"Discoveries #5","url":"https://kiko.io/post/Discoveries-5/","date":"2020-12-19T11:12:44.000Z"},{"title":"Dopamine: How Software should be...","url":"https://kiko.io/post/Dopamine-How-Software-should-be/","date":"2020-07-10T11:49:09.000Z"},{"title":"Folder based publishing in Lightroom","url":"https://kiko.io/post/Folder-based-publishing-in-Lightroom/","date":"2020-10-26T12:28:59.000Z"},{"title":"Horizontal navigation menu above an image","url":"https://kiko.io/post/Horizontal-navigation-menu-above-an-image/","date":"2020-07-20T13:55:47.000Z"},{"title":"Implement source switch for SPA","url":"https://kiko.io/post/Implement-source-switch-for-SPA/","date":"2020-10-04T15:01:02.000Z"},{"title":"Indian Presets for Lightroom","url":"https://kiko.io/post/Indian-Presets-for-Lightroom/","date":"2020-10-28T13:07:20.000Z"},{"title":"Israeli Presets for Lightroom","url":"https://kiko.io/post/Israeli-Presets-for-Lightroom/","date":"2020-10-27T14:07:20.000Z"},{"title":"Localization with resource files in JavaScript web apps","url":"https://kiko.io/post/Localization-with-resource-files-in-JavaScript-web-apps/","date":"2020-06-13T13:49:10.000Z"},{"title":"Pimping the Permalink","url":"https://kiko.io/post/Pimping-the-Permalink/","date":"2020-09-20T14:30:37.000Z"},{"title":"Meaningful automatic versioning with T4","url":"https://kiko.io/post/Meaningful-automatic-versioning-with-T4/","date":"2020-06-27T15:57:18.000Z"},{"title":"Show related posts in Hexo","url":"https://kiko.io/post/Show-related-posts-in-Hexo/","date":"2020-10-03T11:17:03.000Z"},{"title":"TFS/DevOps: Delete Remote Workspace","url":"https://kiko.io/post/TFS-DevOps-Delete-Remote-Workspace/","date":"2020-02-27T23:00:00.000Z"},{"title":"Using GitHub as Commenting Platform","url":"https://kiko.io/post/Using-GitHub-as-Commenting-Platform/","date":"2020-07-05T12:55:16.000Z"},{"title":"Anatomy of Service Worker Communication","url":"https://kiko.io/post/Anatomy-of-Service-Worker-Communication/","date":"2022-11-12T11:26:34.000Z"},{"title":"CSS Columns and Drop Shadow","url":"https://kiko.io/post/CSS-Columns-and-Drop-Shadow/","date":"2022-01-15T23:00:00.000Z"},{"title":"Checker Plus - Gmail in better...","url":"https://kiko.io/post/Checker-Plus-Gmail-in-better/","date":"2022-02-12T23:00:00.000Z"},{"title":"Discoveries #16 - JavaScript","url":"https://kiko.io/post/Discoveries-16-JavaScript/","date":"2022-01-29T12:39:24.000Z"},{"title":"Creating Icon Font from SVG Files","url":"https://kiko.io/post/Creating-Icon-Font-from-SVG-Files/","date":"2022-09-17T13:08:34.000Z"},{"title":"Discoveries #17 - CSS","url":"https://kiko.io/post/Discoveries-17-CSS/","date":"2022-03-11T08:28:10.000Z"},{"title":"Discoveries #18 - JS UI","url":"https://kiko.io/post/Discoveries-18-JS-UI/","date":"2022-05-10T17:22:16.000Z"},{"title":"Discoveries #20 - CSS & UI","url":"https://kiko.io/post/Discoveries-20-CSS-UI/","date":"2022-10-08T11:23:33.000Z"},{"title":"Discoveries #19 - Visual Helpers","url":"https://kiko.io/post/Discoveries-19-Visual-Helpers/","date":"2022-07-09T10:30:17.000Z"},{"title":"Dopamine, a music player for Windows 10 as it should be","url":"https://kiko.io/post/Dopamine-a-music-player-for-Windows-10-as-it-should-be/","date":"2022-08-21T10:00:00.000Z"},{"title":"Discoveries #21 - Sites & Pages","url":"https://kiko.io/post/Discoveries-21-Sites-Pages/","date":"2022-12-28T17:38:22.000Z"},{"title":"Generate Content from Trello","url":"https://kiko.io/post/Generate-Content-from-Trello/","date":"2022-12-29T11:08:00.000Z"},{"title":"Gitpod - Visual Studio Code on the Web","url":"https://kiko.io/post/Gitpod-Visual-Studio-Code-on-the-Web/","date":"2022-07-17T13:00:00.000Z"},{"title":"Mastodon simply explained","url":"https://kiko.io/post/Mastodon-simply-explained/","date":"2022-11-15T15:32:07.000Z"},{"title":"Old Sweetheart Rediscovered","url":"https://kiko.io/post/Old-Sweetheart-Rediscovered/","date":"2022-06-21T11:21:15.000Z"},{"title":"One mouse to rule them all","url":"https://kiko.io/post/One-mouse-to-rule-them-all/","date":"2022-10-08T09:46:57.000Z"},{"title":"Scandinavian Presets for Lightroom","url":"https://kiko.io/post/Scandinavian-Presets-for-Lightroom/","date":"2022-07-19T13:01:37.000Z"},{"title":"Simplest Console File Logger","url":"https://kiko.io/post/Simplest-Console-File-Logger/","date":"2022-06-19T11:31:25.000Z"},{"title":"Syndicate Mastodon Hashtags in your favorite Feed Reader","url":"https://kiko.io/post/Syndicate-Mastodon-Hashtags-in-your-favorite-Feed-Reader/","date":"2022-11-13T16:48:40.000Z"},{"title":"Thanks Dropbox, but I'm off","url":"https://kiko.io/post/Thanks-Dropbox-but-I-m-off/","date":"2022-05-13T17:57:43.000Z"},{"title":"Adding Screenshots to Trello Cards on Android","url":"https://kiko.io/post/Adding-Screenshots-to-Trello-Cards-on-Android/","date":"2021-04-11T14:11:10.000Z"},{"title":"The State of the Blog","url":"https://kiko.io/post/The-State-of-the-Blog/","date":"2022-12-23T21:40:00.000Z"},{"title":"Application-Specific Links on Windows 10","url":"https://kiko.io/post/Application-Specific-Links-on-Windows-10/","date":"2021-09-03T07:31:08.000Z"},{"title":"Automatic Duplicate Image Shadow","url":"https://kiko.io/post/Automatic-Duplicate-Image-Shadow/","date":"2021-07-16T15:45:30.000Z"},{"title":"Croatian Presets for Lightroom","url":"https://kiko.io/post/Croatian-Presets-for-Lightroom/","date":"2021-10-16T13:41:42.000Z"},{"title":"Custom Caller Authentication with ASP.NET Core 5.0 Web API","url":"https://kiko.io/post/Custom-Caller-Authentication-with-ASP-NET-Core-5-0-WebApi/","date":"2021-02-28T16:05:00.000Z"},{"title":"Discoveries #10","url":"https://kiko.io/post/Discoveries-10/","date":"2021-05-24T15:04:26.000Z"},{"title":"Discoveries #11","url":"https://kiko.io/post/Discoveries-11/","date":"2021-06-28T13:03:29.000Z"},{"title":"Discoveries #12 - Tutorials","url":"https://kiko.io/post/Discoveries-12-Tutorials/","date":"2021-07-30T08:41:51.000Z"},{"title":"Discoveries #13","url":"https://kiko.io/post/Discoveries-13/","date":"2021-09-29T10:57:51.000Z"},{"title":"Discoveries #14","url":"https://kiko.io/post/Discoveries-14/","date":"2021-11-20T10:14:29.000Z"},{"title":"Discoveries #15 - Self Hosted","url":"https://kiko.io/post/Discoveries-15-Self-Hosted/","date":"2021-12-25T11:18:51.000Z"},{"title":"Discoveries #6","url":"https://kiko.io/post/Discoveries-6/","date":"2021-01-20T14:01:34.000Z"},{"title":"Discoveries #7","url":"https://kiko.io/post/Discoveries-7/","date":"2021-02-25T13:36:10.000Z"},{"title":"Discoveries #8","url":"https://kiko.io/post/Discoveries-8/","date":"2021-03-31T11:41:26.000Z"},{"title":"Discoveries #9","url":"https://kiko.io/post/Discoveries-9/","date":"2021-04-20T09:02:14.000Z"},{"title":"Forking Hexo plugin 'hexo-index-anything'","url":"https://kiko.io/post/Forking-Hexo-plugin-hexo-index-anything/","date":"2021-04-25T11:41:46.000Z"},{"title":"Generate Social Media Images Automatically","url":"https://kiko.io/post/Generate-Social-Media-Images-Automatically/","date":"2021-07-10T09:07:31.000Z"},{"title":"GitHub Tag Plugins for Hexo","url":"https://kiko.io/post/GitHub-Tag-Plugins-for-Hexo/","date":"2021-12-29T11:20:34.000Z"},{"title":"GitHubs Magic Dot","url":"https://kiko.io/post/GitHubs-Magic-Dot/","date":"2021-08-20T11:43:04.000Z"},{"title":"Hexo and the IndieWeb (Receiving Webmentions)","url":"https://kiko.io/post/Hexo-and-the-IndieWeb-Receiving-Webmentions/","date":"2021-05-13T12:16:00.000Z"},{"title":"Hexo Tag Plugin Collection","url":"https://kiko.io/post/Hexo-Tag-Plugin-Collection/","date":"2021-12-12T13:07:36.000Z"},{"title":"Hexo and the IndieWeb (Sending Webmentions)","url":"https://kiko.io/post/Hexo-and-the-IndieWeb-Sending-Webmentions/","date":"2021-05-08T17:39:43.000Z"},{"title":"Hexo and the IndieWeb","url":"https://kiko.io/post/Hexo-and-the-IndieWeb/","date":"2021-05-05T17:15:00.000Z"},{"title":"How to prevent duplicate events","url":"https://kiko.io/post/How-to-prevent-duplicate-events/","date":"2021-01-07T16:22:25.000Z"},{"title":"Native JavaScript Multilanguage Templating","url":"https://kiko.io/post/Native-JavaScript-Multilanguage-Templating/","date":"2021-02-24T12:31:58.000Z"},{"title":"Open Source Insights - Seeing the big picture","url":"https://kiko.io/post/Open-Source-Insights-Seeing-the-big-picture/","date":"2021-06-06T07:36:49.000Z"},{"title":"Pattern for dynamic Hexo pages","url":"https://kiko.io/post/Pattern-for-dynamic-Hexo-pages/","date":"2021-08-25T07:43:44.000Z"},{"title":"Photo Workflow Re-Thought","url":"https://kiko.io/post/Photo-Workflow-Re-Thought/","date":"2021-09-12T16:16:48.000Z"},{"title":"Remote Testing and Debugging with Chrome","url":"https://kiko.io/post/Remote-Testing-and-Debugging-with-Chrome/","date":"2021-01-24T13:47:10.000Z"},{"title":"Running Rollup with Gulp","url":"https://kiko.io/post/Running-Rollup-with-Gulp/","date":"2021-07-29T13:34:03.000Z"},{"title":"SVG Resources","url":"https://kiko.io/post/SVG-Resources/","date":"2021-04-09T12:07:53.000Z"},{"title":"Safely remove multiple classes using a prefix","url":"https://kiko.io/post/Safely-remove-multiple-classes-using-a-prefix/","date":"2021-01-18T10:17:46.000Z"},{"title":"Scotch Presets for Lightroom","url":"https://kiko.io/post/Scotch-Presets-for-Lightroom/","date":"2021-04-18T17:51:35.000Z"},{"title":"Spice Up Windows Terminal","url":"https://kiko.io/post/Spice-Up-Windows-Terminal/","date":"2021-04-16T09:55:15.000Z"},{"title":"The Last Image Gallery...","url":"https://kiko.io/post/The-Last-Image-Gallery/","date":"2021-10-10T10:28:09.000Z"},{"title":"Triangulate your images with Triangula","url":"https://kiko.io/post/Triangulate-your-images-with-Triangula/","date":"2021-04-30T12:56:13.000Z"},{"title":"Tringula And The Beauty Of Mathematics","url":"https://kiko.io/post/Tringula-And-The-Beauty-Of-Mathematics/","date":"2021-12-07T13:45:00.000Z"},{"title":"Use a duplicate image to drop a shadow","url":"https://kiko.io/post/Use-a-duplicate-image-to-drop-a-shadow/","date":"2021-01-20T09:38:50.000Z"},{"title":"Use and manage multiple Node.js versions on Windows 10","url":"https://kiko.io/post/Use-and-manage-multiple-Node-js-versions-on-Windows-10/","date":"2021-01-08T12:47:22.000Z"},{"title":"Utilize a repository of reusable ES6 template literals","url":"https://kiko.io/post/Utilize-a-repository-of-reusable-ES6-template-literals/","date":"2021-01-03T12:29:01.000Z"},{"title":"Uups ... empty posts","url":"https://kiko.io/post/Uups-empty-posts/","date":"2021-09-21T11:40:25.000Z"},{"title":"VS Code on the Web","url":"https://kiko.io/post/VS-Code-on-the-Web/","date":"2021-10-22T16:38:29.000Z"},{"title":"Visualize the codebase of your GitHub repo","url":"https://kiko.io/post/Visualize-the-codebase-of-your-GitHub-repo/","date":"2021-08-21T05:55:00.000Z"},{"title":"Add Link to Trello on Android via Share Menu","url":"https://kiko.io/post/Add-Link-to-Trello-on-Android-via-Share-Menu/","date":"2023-11-13T16:11:47.000Z"},{"title":"Breton Presets for Lightroom","url":"https://kiko.io/post/Breton-Presets-for-Lightroom/","date":"2023-03-10T11:04:05.000Z"},{"title":"CONTINUE READING Link & Auto Scrolling on the called page","url":"https://kiko.io/post/CONTINUE-READING-Link-Auto-Scrolling-on-the-called-page/","date":"2023-07-29T14:14:35.000Z"},{"title":"Contribute with Conventional Commits","url":"https://kiko.io/post/Contribute-with-Conventional-Commits/","date":"2023-09-15T15:57:40.000Z"},{"title":"Convert HTML into Plain Text in Hexo","url":"https://kiko.io/post/Convert-HTML-into-Plain-Text-in-Hexo/","date":"2023-08-31T19:58:36.000Z"},{"title":"Discoveries #22 - Tips/Tricks","url":"https://kiko.io/post/Discoveries-22-Tips-Tricks/","date":"2023-01-06T14:05:55.000Z"},{"title":"Discoveries #23 - UI/CSS","url":"https://kiko.io/post/Discoveries-23-UI-CSS/","date":"2023-02-12T10:45:10.000Z"},{"title":"Discoveries #24 - JavaScript & UI","url":"https://kiko.io/post/Discoveries-24-JavaScript-UI/","date":"2023-04-28T14:39:01.000Z"},{"title":"Discoveries #25 - Tutorials & HowTo's","url":"https://kiko.io/post/Discoveries-25-Tutorials-HowTo-s/","date":"2023-07-01T11:19:22.000Z"},{"title":"Discoveries #26 - JavaScript HowTo's","url":"https://kiko.io/post/Discoveries-26-JavaScript-HowTo-s/","date":"2023-09-12T05:53:45.000Z"},{"title":"Discoveries #27 - JavaScript Tools","url":"https://kiko.io/post/Discoveries-27-JavaScript-Tools/","date":"2023-11-20T14:16:39.000Z"},{"title":"Extension of downupPopup: Back Button, Escape Key & More","url":"https://kiko.io/post/Extension-of-downupPopup-Back-Button-Escape-Key-More/","date":"2023-07-04T11:07:38.000Z"},{"title":"Experimenting with the font LEXEND","url":"https://kiko.io/post/Experimenting-with-the-font-LEXEND/","date":"2023-11-12T17:24:00.000Z"},{"title":"Favourite Pens of 2022","url":"https://kiko.io/post/Favourite-Pens-of-2022/","date":"2023-01-14T12:10:48.000Z"},{"title":"Get and use a dominant color that matches the header image","url":"https://kiko.io/post/Get-and-use-a-dominant-color-that-matches-the-header-image/","date":"2023-10-10T13:34:57.000Z"},{"title":"Handling IPTC metadata on Android and Windows","url":"https://kiko.io/post/Handling-IPTC-metadata-on-Android-and-Windows/","date":"2023-10-28T12:00:55.000Z"},{"title":"Hexo, WebFinger and better discoverability","url":"https://kiko.io/post/Hexo-Webfinger-and-better-discoverability/","date":"2023-12-02T13:19:03.000Z"},{"title":"Image Masonry Tag Plugin for Hexo","url":"https://kiko.io/post/Image-Masonry-Tag-Plugin-for-Hexo/","date":"2023-09-01T14:07:37.000Z"},{"title":"Include and provide JSON data in Hexo EJS Templates","url":"https://kiko.io/post/Include-and-provide-JSON-data-in-Hexo-EJS-Templates/","date":"2023-06-27T05:26:21.000Z"},{"title":"Integration of Pagefind in Hexo","url":"https://kiko.io/post/Integration-of-Pagefind-in-Hexo/","date":"2023-01-19T12:24:00.000Z"},{"title":"Majorcan Details","url":"https://kiko.io/post/Majorcan-Details/","date":"2023-10-03T11:52:10.000Z"},{"title":"Mecklenburg Lakes","url":"https://kiko.io/post/Mecklenburg-Lakes/","date":"2023-11-26T13:59:17.000Z"},{"title":"Mastodon Share Bottom Sheet Dialog","url":"https://kiko.io/post/Mastodon-Share-Bottom-Sheet-Dialog/","date":"2023-09-28T07:57:54.000Z"},{"title":"My Hometown, My Club","url":"https://kiko.io/post/My-Hometown-My-Club/","date":"2023-08-19T13:59:18.000Z"},{"title":"Pagefind UI and URL Parameters","url":"https://kiko.io/post/Pagefind-UI-and-URL-Parameters/","date":"2023-02-03T10:51:00.000Z"},{"title":"Pool Photo Generator","url":"https://kiko.io/post/Pool-Photo-Generator/","date":"2023-08-20T22:00:00.000Z"},{"title":"Provide Blog Metadata via JSON-LD","url":"https://kiko.io/post/Provide-Blog-Metadata-via-JSON-LD/","date":"2023-02-09T23:00:00.000Z"},{"title":"Radio Garden","url":"https://kiko.io/post/Radio-Garden/","date":"2023-02-01T23:00:00.000Z"},{"title":"SGE vs. HJK @ 2023-10-26","url":"https://kiko.io/post/SGE-vs-HJK-2023-10-26/","date":"2023-10-29T18:59:05.000Z"},{"title":"SVWW vs. Braunschweig @ 2023-12-08","url":"https://kiko.io/post/SVWW-vs-Braunschweig-2023-12-08/","date":"2023-12-10T16:15:16.000Z"},{"title":"SVWW vs. HSV @ 2023-10-07","url":"https://kiko.io/post/SVWW-vs-HSV-2023-10-07/","date":"2023-10-07T15:46:04.000Z"},{"title":"SVWW vs. Hansa Rostock @ 2023-10-29","url":"https://kiko.io/post/SVWW-vs-Hansa-Rostock-2023-10-29/","date":"2023-10-30T17:56:08.000Z"},{"title":"SVWW vs. Kaiserslautern @ 2023-11-12","url":"https://kiko.io/post/SVWW-vs-Kaiserslautern-2023-11-12/","date":"2023-11-15T13:12:00.000Z"},{"title":"SVWW vs. Karlsruhe @ 2023-08-18","url":"https://kiko.io/post/SVWW-vs-Karlsruhe-2023-08-18/","date":"2023-08-19T16:33:00.000Z"},{"title":"SVWW vs. Schalke @ 2023-09-02","url":"https://kiko.io/post/SVWW-vs-Schalke-2023-09-02/","date":"2023-09-02T15:24:24.000Z"},{"title":"Show pages meta data (JSON-LD) in Bottom Sheet","url":"https://kiko.io/post/Show-pages-meta-data-JSON-LD-in-Bottom-Sheet/","date":"2023-06-11T12:11:45.000Z"},{"title":"Speyer Automotive","url":"https://kiko.io/post/Speyer-Automotive/","date":"2023-11-15T15:13:13.000Z"},{"title":"Versengold in Concert","url":"https://kiko.io/post/Versengold-in-Concert/","date":"2023-09-10T08:59:19.000Z"},{"title":"Top 10 Pens of Jon Kantner","url":"https://kiko.io/post/Top-10-Pens-of-Jon-Kantner/","date":"2023-05-08T13:34:15.000Z"}],"pages":[{"title":"","url":"https://kiko.io/index.html","date":"2022-12-30T23:00:00.000Z"},{"title":"404","url":"https://kiko.io/404.html","date":"2020-09-23T10:31:40.000Z"},{"title":"Impressum","url":"https://kiko.io/impressum/index.html","date":"2023-01-09T18:48:35.000Z"},{"title":"","url":"https://kiko.io/downloads/code/test.js","date":"2023-12-12T13:27:43.622Z"}]}
\ No newline at end of file
+{"tags":["2. Bundesliga","ADSI","Android","Audio","Authentication","Blogging","Browser","Bundling","C#","CSS","Collection","Concert","Contributing","DOM","Dark Mode","Database Project","Debugging","ES6","Eintracht","Error","Events","Fediverse","Font","Git/GitHub","GitHub","Hexo","Hosting","Identity","Imaging","IndieWeb","JSON-LD","JavaScript","Lightroom","Localization","Logging","Mail","Mastodon","MediaQuery","Meta","Metadata","Node.js","Office","PWA","Plugin","PowerShell","Presets","Publishing","Rant","Remote","Resource","SPA","SVG","SVWW","Search","Share","Social Media","Stylus","T4","TFS/DevOps","Templating","Theming","Trello","Tutorial","UI","Usability","VS Code","Versioning","Visual Studio","WebAPI","Windows","Workflow","jQuery"],"categories":[".NET","Football","JavaScript","Misc","Photo","SQL","Tools","UI/UX"],"posts":[{"title":"A New Blog: Blogging and Synching en route","url":"https://kiko.io/post/A-New-Blog-Blogging-and-Synching-en-route/","date":"2019-09-30T22:00:00.000Z"},{"title":"A New Blog: Customizing Hexo","url":"https://kiko.io/post/A-New-Blog-Customizing-Hexo/","date":"2019-09-25T10:00:00.000Z"},{"title":"A New Blog: VS Code, Hexo and GitHub Pages","url":"https://kiko.io/post/A-New-Blog-VS-Code-Hexo-and-GitHub-Pages/","date":"2019-09-23T22:00:00.000Z"},{"title":"Hexo and the Dark Mode ... revised","url":"https://kiko.io/post/Hexo-and-the-Dark-Mode-revised/","date":"2019-10-26T12:08:05.000Z"},{"title":"Better Input Change Event","url":"https://kiko.io/post/Better-Input-Change-Event/","date":"2019-11-26T15:51:17.000Z"},{"title":"Hexo and the Dark Mode","url":"https://kiko.io/post/Hexo-and-the-Dark-Mode/","date":"2019-10-23T13:28:04.000Z"},{"title":"How-To: Visual Studio Database Project and ADSI","url":"https://kiko.io/post/How-To-Visual-Studio-Database-Project-and-ADSI/","date":"2019-09-17T10:00:01.000Z"},{"title":"Add website to Trello card the better way","url":"https://kiko.io/post/Add-website-to-Trello-card-the-better-way/","date":"2020-09-07T13:17:21.000Z"},{"title":"404 Page in Hexo for GitHub Pages","url":"https://kiko.io/post/404-Page-in-Hexo-for-GitHub-Pages/","date":"2020-09-23T12:28:50.000Z"},{"title":"Automatic Header Images in Hexo","url":"https://kiko.io/post/Automatic-Header-Images-in-Hexo/","date":"2020-06-22T15:49:16.000Z"},{"title":"Change CSS class when element scrolls into viewport","url":"https://kiko.io/post/Change-CSS-class-when-element-scrolls-into-viewport/","date":"2020-07-13T16:24:39.000Z"},{"title":"Device Class Detection in JavaScript","url":"https://kiko.io/post/Device-Class-Detection-in-JavaScript/","date":"2020-09-28T13:27:17.000Z"},{"title":"Discoveries #1","url":"https://kiko.io/post/Discoveries-1/","date":"2020-07-12T07:55:10.000Z"},{"title":"Discoveries #2","url":"https://kiko.io/post/Discoveries-2/","date":"2020-09-07T15:54:50.000Z"},{"title":"Discoveries #3 - Tutorials","url":"https://kiko.io/post/Discoveries-3-Tutorials/","date":"2020-09-29T10:02:10.000Z"},{"title":"Discoveries #4","url":"https://kiko.io/post/Discoveries-4/","date":"2020-10-10T13:27:47.000Z"},{"title":"Discoveries #5","url":"https://kiko.io/post/Discoveries-5/","date":"2020-12-19T11:12:44.000Z"},{"title":"Dopamine: How Software should be...","url":"https://kiko.io/post/Dopamine-How-Software-should-be/","date":"2020-07-10T11:49:09.000Z"},{"title":"Folder based publishing in Lightroom","url":"https://kiko.io/post/Folder-based-publishing-in-Lightroom/","date":"2020-10-26T12:28:59.000Z"},{"title":"Horizontal navigation menu above an image","url":"https://kiko.io/post/Horizontal-navigation-menu-above-an-image/","date":"2020-07-20T13:55:47.000Z"},{"title":"Implement source switch for SPA","url":"https://kiko.io/post/Implement-source-switch-for-SPA/","date":"2020-10-04T15:01:02.000Z"},{"title":"Indian Presets for Lightroom","url":"https://kiko.io/post/Indian-Presets-for-Lightroom/","date":"2020-10-28T13:07:20.000Z"},{"title":"Israeli Presets for Lightroom","url":"https://kiko.io/post/Israeli-Presets-for-Lightroom/","date":"2020-10-27T14:07:20.000Z"},{"title":"Localization with resource files in JavaScript web apps","url":"https://kiko.io/post/Localization-with-resource-files-in-JavaScript-web-apps/","date":"2020-06-13T13:49:10.000Z"},{"title":"Pimping the Permalink","url":"https://kiko.io/post/Pimping-the-Permalink/","date":"2020-09-20T14:30:37.000Z"},{"title":"Meaningful automatic versioning with T4","url":"https://kiko.io/post/Meaningful-automatic-versioning-with-T4/","date":"2020-06-27T15:57:18.000Z"},{"title":"Show related posts in Hexo","url":"https://kiko.io/post/Show-related-posts-in-Hexo/","date":"2020-10-03T11:17:03.000Z"},{"title":"TFS/DevOps: Delete Remote Workspace","url":"https://kiko.io/post/TFS-DevOps-Delete-Remote-Workspace/","date":"2020-02-27T23:00:00.000Z"},{"title":"Using GitHub as Commenting Platform","url":"https://kiko.io/post/Using-GitHub-as-Commenting-Platform/","date":"2020-07-05T12:55:16.000Z"},{"title":"Anatomy of Service Worker Communication","url":"https://kiko.io/post/Anatomy-of-Service-Worker-Communication/","date":"2022-11-12T11:26:34.000Z"},{"title":"CSS Columns and Drop Shadow","url":"https://kiko.io/post/CSS-Columns-and-Drop-Shadow/","date":"2022-01-15T23:00:00.000Z"},{"title":"Checker Plus - Gmail in better...","url":"https://kiko.io/post/Checker-Plus-Gmail-in-better/","date":"2022-02-12T23:00:00.000Z"},{"title":"Discoveries #16 - JavaScript","url":"https://kiko.io/post/Discoveries-16-JavaScript/","date":"2022-01-29T12:39:24.000Z"},{"title":"Creating Icon Font from SVG Files","url":"https://kiko.io/post/Creating-Icon-Font-from-SVG-Files/","date":"2022-09-17T13:08:34.000Z"},{"title":"Discoveries #17 - CSS","url":"https://kiko.io/post/Discoveries-17-CSS/","date":"2022-03-11T08:28:10.000Z"},{"title":"Discoveries #19 - Visual Helpers","url":"https://kiko.io/post/Discoveries-19-Visual-Helpers/","date":"2022-07-09T10:30:17.000Z"},{"title":"Discoveries #20 - CSS & UI","url":"https://kiko.io/post/Discoveries-20-CSS-UI/","date":"2022-10-08T11:23:33.000Z"},{"title":"Discoveries #18 - JS UI","url":"https://kiko.io/post/Discoveries-18-JS-UI/","date":"2022-05-10T17:22:16.000Z"},{"title":"Discoveries #21 - Sites & Pages","url":"https://kiko.io/post/Discoveries-21-Sites-Pages/","date":"2022-12-28T17:38:22.000Z"},{"title":"Dopamine, a music player for Windows 10 as it should be","url":"https://kiko.io/post/Dopamine-a-music-player-for-Windows-10-as-it-should-be/","date":"2022-08-21T10:00:00.000Z"},{"title":"Generate Content from Trello","url":"https://kiko.io/post/Generate-Content-from-Trello/","date":"2022-12-29T11:08:00.000Z"},{"title":"Gitpod - Visual Studio Code on the Web","url":"https://kiko.io/post/Gitpod-Visual-Studio-Code-on-the-Web/","date":"2022-07-17T13:00:00.000Z"},{"title":"Mastodon simply explained","url":"https://kiko.io/post/Mastodon-simply-explained/","date":"2022-11-15T15:32:07.000Z"},{"title":"Old Sweetheart Rediscovered","url":"https://kiko.io/post/Old-Sweetheart-Rediscovered/","date":"2022-06-21T11:21:15.000Z"},{"title":"Scandinavian Presets for Lightroom","url":"https://kiko.io/post/Scandinavian-Presets-for-Lightroom/","date":"2022-07-19T13:01:37.000Z"},{"title":"One mouse to rule them all","url":"https://kiko.io/post/One-mouse-to-rule-them-all/","date":"2022-10-08T09:46:57.000Z"},{"title":"Simplest Console File Logger","url":"https://kiko.io/post/Simplest-Console-File-Logger/","date":"2022-06-19T11:31:25.000Z"},{"title":"Syndicate Mastodon Hashtags in your favorite Feed Reader","url":"https://kiko.io/post/Syndicate-Mastodon-Hashtags-in-your-favorite-Feed-Reader/","date":"2022-11-13T16:48:40.000Z"},{"title":"Thanks Dropbox, but I'm off","url":"https://kiko.io/post/Thanks-Dropbox-but-I-m-off/","date":"2022-05-13T17:57:43.000Z"},{"title":"Adding Screenshots to Trello Cards on Android","url":"https://kiko.io/post/Adding-Screenshots-to-Trello-Cards-on-Android/","date":"2021-04-11T14:11:10.000Z"},{"title":"The State of the Blog","url":"https://kiko.io/post/The-State-of-the-Blog/","date":"2022-12-23T21:40:00.000Z"},{"title":"Application-Specific Links on Windows 10","url":"https://kiko.io/post/Application-Specific-Links-on-Windows-10/","date":"2021-09-03T07:31:08.000Z"},{"title":"Automatic Duplicate Image Shadow","url":"https://kiko.io/post/Automatic-Duplicate-Image-Shadow/","date":"2021-07-16T15:45:30.000Z"},{"title":"Croatian Presets for Lightroom","url":"https://kiko.io/post/Croatian-Presets-for-Lightroom/","date":"2021-10-16T13:41:42.000Z"},{"title":"Custom Caller Authentication with ASP.NET Core 5.0 Web API","url":"https://kiko.io/post/Custom-Caller-Authentication-with-ASP-NET-Core-5-0-WebApi/","date":"2021-02-28T16:05:00.000Z"},{"title":"Discoveries #10","url":"https://kiko.io/post/Discoveries-10/","date":"2021-05-24T15:04:26.000Z"},{"title":"Discoveries #11","url":"https://kiko.io/post/Discoveries-11/","date":"2021-06-28T13:03:29.000Z"},{"title":"Discoveries #12 - Tutorials","url":"https://kiko.io/post/Discoveries-12-Tutorials/","date":"2021-07-30T08:41:51.000Z"},{"title":"Discoveries #13","url":"https://kiko.io/post/Discoveries-13/","date":"2021-09-29T10:57:51.000Z"},{"title":"Discoveries #14","url":"https://kiko.io/post/Discoveries-14/","date":"2021-11-20T10:14:29.000Z"},{"title":"Discoveries #15 - Self Hosted","url":"https://kiko.io/post/Discoveries-15-Self-Hosted/","date":"2021-12-25T11:18:51.000Z"},{"title":"Discoveries #6","url":"https://kiko.io/post/Discoveries-6/","date":"2021-01-20T14:01:34.000Z"},{"title":"Discoveries #7","url":"https://kiko.io/post/Discoveries-7/","date":"2021-02-25T13:36:10.000Z"},{"title":"Discoveries #8","url":"https://kiko.io/post/Discoveries-8/","date":"2021-03-31T11:41:26.000Z"},{"title":"Discoveries #9","url":"https://kiko.io/post/Discoveries-9/","date":"2021-04-20T09:02:14.000Z"},{"title":"Forking Hexo plugin 'hexo-index-anything'","url":"https://kiko.io/post/Forking-Hexo-plugin-hexo-index-anything/","date":"2021-04-25T11:41:46.000Z"},{"title":"GitHub Tag Plugins for Hexo","url":"https://kiko.io/post/GitHub-Tag-Plugins-for-Hexo/","date":"2021-12-29T11:20:34.000Z"},{"title":"Generate Social Media Images Automatically","url":"https://kiko.io/post/Generate-Social-Media-Images-Automatically/","date":"2021-07-10T09:07:31.000Z"},{"title":"GitHubs Magic Dot","url":"https://kiko.io/post/GitHubs-Magic-Dot/","date":"2021-08-20T11:43:04.000Z"},{"title":"Hexo Tag Plugin Collection","url":"https://kiko.io/post/Hexo-Tag-Plugin-Collection/","date":"2021-12-12T13:07:36.000Z"},{"title":"Hexo and the IndieWeb (Receiving Webmentions)","url":"https://kiko.io/post/Hexo-and-the-IndieWeb-Receiving-Webmentions/","date":"2021-05-13T12:16:00.000Z"},{"title":"Hexo and the IndieWeb (Sending Webmentions)","url":"https://kiko.io/post/Hexo-and-the-IndieWeb-Sending-Webmentions/","date":"2021-05-08T17:39:43.000Z"},{"title":"Hexo and the IndieWeb","url":"https://kiko.io/post/Hexo-and-the-IndieWeb/","date":"2021-05-05T17:15:00.000Z"},{"title":"How to prevent duplicate events","url":"https://kiko.io/post/How-to-prevent-duplicate-events/","date":"2021-01-07T16:22:25.000Z"},{"title":"Native JavaScript Multilanguage Templating","url":"https://kiko.io/post/Native-JavaScript-Multilanguage-Templating/","date":"2021-02-24T12:31:58.000Z"},{"title":"Open Source Insights - Seeing the big picture","url":"https://kiko.io/post/Open-Source-Insights-Seeing-the-big-picture/","date":"2021-06-06T07:36:49.000Z"},{"title":"Photo Workflow Re-Thought","url":"https://kiko.io/post/Photo-Workflow-Re-Thought/","date":"2021-09-12T16:16:48.000Z"},{"title":"Pattern for dynamic Hexo pages","url":"https://kiko.io/post/Pattern-for-dynamic-Hexo-pages/","date":"2021-08-25T07:43:44.000Z"},{"title":"Remote Testing and Debugging with Chrome","url":"https://kiko.io/post/Remote-Testing-and-Debugging-with-Chrome/","date":"2021-01-24T13:47:10.000Z"},{"title":"Running Rollup with Gulp","url":"https://kiko.io/post/Running-Rollup-with-Gulp/","date":"2021-07-29T13:34:03.000Z"},{"title":"SVG Resources","url":"https://kiko.io/post/SVG-Resources/","date":"2021-04-09T12:07:53.000Z"},{"title":"Scotch Presets for Lightroom","url":"https://kiko.io/post/Scotch-Presets-for-Lightroom/","date":"2021-04-18T17:51:35.000Z"},{"title":"Safely remove multiple classes using a prefix","url":"https://kiko.io/post/Safely-remove-multiple-classes-using-a-prefix/","date":"2021-01-18T10:17:46.000Z"},{"title":"Spice Up Windows Terminal","url":"https://kiko.io/post/Spice-Up-Windows-Terminal/","date":"2021-04-16T09:55:15.000Z"},{"title":"Triangulate your images with Triangula","url":"https://kiko.io/post/Triangulate-your-images-with-Triangula/","date":"2021-04-30T12:56:13.000Z"},{"title":"The Last Image Gallery...","url":"https://kiko.io/post/The-Last-Image-Gallery/","date":"2021-10-10T10:28:09.000Z"},{"title":"Tringula And The Beauty Of Mathematics","url":"https://kiko.io/post/Tringula-And-The-Beauty-Of-Mathematics/","date":"2021-12-07T13:45:00.000Z"},{"title":"Use a duplicate image to drop a shadow","url":"https://kiko.io/post/Use-a-duplicate-image-to-drop-a-shadow/","date":"2021-01-20T09:38:50.000Z"},{"title":"Use and manage multiple Node.js versions on Windows 10","url":"https://kiko.io/post/Use-and-manage-multiple-Node-js-versions-on-Windows-10/","date":"2021-01-08T12:47:22.000Z"},{"title":"VS Code on the Web","url":"https://kiko.io/post/VS-Code-on-the-Web/","date":"2021-10-22T16:38:29.000Z"},{"title":"Utilize a repository of reusable ES6 template literals","url":"https://kiko.io/post/Utilize-a-repository-of-reusable-ES6-template-literals/","date":"2021-01-03T12:29:01.000Z"},{"title":"Uups ... empty posts","url":"https://kiko.io/post/Uups-empty-posts/","date":"2021-09-21T11:40:25.000Z"},{"title":"Visualize the codebase of your GitHub repo","url":"https://kiko.io/post/Visualize-the-codebase-of-your-GitHub-repo/","date":"2021-08-21T05:55:00.000Z"},{"title":"Add Link to Trello on Android via Share Menu","url":"https://kiko.io/post/Add-Link-to-Trello-on-Android-via-Share-Menu/","date":"2023-11-13T16:11:47.000Z"},{"title":"Breton Presets for Lightroom","url":"https://kiko.io/post/Breton-Presets-for-Lightroom/","date":"2023-03-10T11:04:05.000Z"},{"title":"CONTINUE READING Link & Auto Scrolling on the called page","url":"https://kiko.io/post/CONTINUE-READING-Link-Auto-Scrolling-on-the-called-page/","date":"2023-07-29T14:14:35.000Z"},{"title":"Contribute with Conventional Commits","url":"https://kiko.io/post/Contribute-with-Conventional-Commits/","date":"2023-09-15T15:57:40.000Z"},{"title":"Convert HTML into Plain Text in Hexo","url":"https://kiko.io/post/Convert-HTML-into-Plain-Text-in-Hexo/","date":"2023-08-31T19:58:36.000Z"},{"title":"Discoveries #22 - Tips/Tricks","url":"https://kiko.io/post/Discoveries-22-Tips-Tricks/","date":"2023-01-06T14:05:55.000Z"},{"title":"Discoveries #23 - UI/CSS","url":"https://kiko.io/post/Discoveries-23-UI-CSS/","date":"2023-02-12T10:45:10.000Z"},{"title":"Discoveries #24 - JavaScript & UI","url":"https://kiko.io/post/Discoveries-24-JavaScript-UI/","date":"2023-04-28T14:39:01.000Z"},{"title":"Discoveries #25 - Tutorials & HowTo's","url":"https://kiko.io/post/Discoveries-25-Tutorials-HowTo-s/","date":"2023-07-01T11:19:22.000Z"},{"title":"Discoveries #26 - JavaScript HowTo's","url":"https://kiko.io/post/Discoveries-26-JavaScript-HowTo-s/","date":"2023-09-12T05:53:45.000Z"},{"title":"Discoveries #27 - JavaScript Tools","url":"https://kiko.io/post/Discoveries-27-JavaScript-Tools/","date":"2023-11-20T14:16:39.000Z"},{"title":"Experimenting with the font LEXEND","url":"https://kiko.io/post/Experimenting-with-the-font-LEXEND/","date":"2023-11-12T17:24:00.000Z"},{"title":"Extension of downupPopup: Back Button, Escape Key & More","url":"https://kiko.io/post/Extension-of-downupPopup-Back-Button-Escape-Key-More/","date":"2023-07-04T11:07:38.000Z"},{"title":"Favourite Pens of 2022","url":"https://kiko.io/post/Favourite-Pens-of-2022/","date":"2023-01-14T12:10:48.000Z"},{"title":"Get and use a dominant color that matches the header image","url":"https://kiko.io/post/Get-and-use-a-dominant-color-that-matches-the-header-image/","date":"2023-10-10T13:34:57.000Z"},{"title":"Handling IPTC metadata on Android and Windows","url":"https://kiko.io/post/Handling-IPTC-metadata-on-Android-and-Windows/","date":"2023-10-28T12:00:55.000Z"},{"title":"Hexo, WebFinger and better discoverability","url":"https://kiko.io/post/Hexo-Webfinger-and-better-discoverability/","date":"2023-12-02T13:19:03.000Z"},{"title":"Include and provide JSON data in Hexo EJS Templates","url":"https://kiko.io/post/Include-and-provide-JSON-data-in-Hexo-EJS-Templates/","date":"2023-06-27T05:26:21.000Z"},{"title":"Image Masonry Tag Plugin for Hexo","url":"https://kiko.io/post/Image-Masonry-Tag-Plugin-for-Hexo/","date":"2023-09-01T14:07:37.000Z"},{"title":"Majorcan Details","url":"https://kiko.io/post/Majorcan-Details/","date":"2023-10-03T11:52:10.000Z"},{"title":"Integration of Pagefind in Hexo","url":"https://kiko.io/post/Integration-of-Pagefind-in-Hexo/","date":"2023-01-19T12:24:00.000Z"},{"title":"Mastodon Share Bottom Sheet Dialog","url":"https://kiko.io/post/Mastodon-Share-Bottom-Sheet-Dialog/","date":"2023-09-28T07:57:54.000Z"},{"title":"Mecklenburg Lakes","url":"https://kiko.io/post/Mecklenburg-Lakes/","date":"2023-11-26T13:59:17.000Z"},{"title":"My Hometown, My Club","url":"https://kiko.io/post/My-Hometown-My-Club/","date":"2023-08-19T13:59:18.000Z"},{"title":"Pagefind UI and URL Parameters","url":"https://kiko.io/post/Pagefind-UI-and-URL-Parameters/","date":"2023-02-03T10:51:00.000Z"},{"title":"Pool Photo Generator","url":"https://kiko.io/post/Pool-Photo-Generator/","date":"2023-08-20T22:00:00.000Z"},{"title":"Provide Blog Metadata via JSON-LD","url":"https://kiko.io/post/Provide-Blog-Metadata-via-JSON-LD/","date":"2023-02-09T23:00:00.000Z"},{"title":"Radio Garden","url":"https://kiko.io/post/Radio-Garden/","date":"2023-02-01T23:00:00.000Z"},{"title":"SGE vs. HJK @ 2023-10-26","url":"https://kiko.io/post/SGE-vs-HJK-2023-10-26/","date":"2023-10-29T18:59:05.000Z"},{"title":"SVWW vs. Braunschweig @ 2023-12-08","url":"https://kiko.io/post/SVWW-vs-Braunschweig-2023-12-08/","date":"2023-12-10T16:15:16.000Z"},{"title":"SVWW vs. Hansa Rostock @ 2023-10-29","url":"https://kiko.io/post/SVWW-vs-Hansa-Rostock-2023-10-29/","date":"2023-10-30T17:56:08.000Z"},{"title":"SVWW vs. HSV @ 2023-10-07","url":"https://kiko.io/post/SVWW-vs-HSV-2023-10-07/","date":"2023-10-07T15:46:04.000Z"},{"title":"SVWW vs. Karlsruhe @ 2023-08-18","url":"https://kiko.io/post/SVWW-vs-Karlsruhe-2023-08-18/","date":"2023-08-19T16:33:00.000Z"},{"title":"SVWW vs. Kaiserslautern @ 2023-11-12","url":"https://kiko.io/post/SVWW-vs-Kaiserslautern-2023-11-12/","date":"2023-11-15T13:12:00.000Z"},{"title":"SVWW vs. Schalke @ 2023-09-02","url":"https://kiko.io/post/SVWW-vs-Schalke-2023-09-02/","date":"2023-09-02T15:24:24.000Z"},{"title":"Speyer Automotive","url":"https://kiko.io/post/Speyer-Automotive/","date":"2023-11-15T15:13:13.000Z"},{"title":"Show pages meta data (JSON-LD) in Bottom Sheet","url":"https://kiko.io/post/Show-pages-meta-data-JSON-LD-in-Bottom-Sheet/","date":"2023-06-11T12:11:45.000Z"},{"title":"Top 10 Pens of Jon Kantner","url":"https://kiko.io/post/Top-10-Pens-of-Jon-Kantner/","date":"2023-05-08T13:34:15.000Z"},{"title":"Versengold in Concert","url":"https://kiko.io/post/Versengold-in-Concert/","date":"2023-09-10T08:59:19.000Z"}],"pages":[{"title":"","url":"https://kiko.io/index.html","date":"2022-12-30T23:00:00.000Z"},{"title":"404","url":"https://kiko.io/404.html","date":"2020-09-23T10:31:40.000Z"},{"title":"Impressum","url":"https://kiko.io/impressum/index.html","date":"2023-01-09T18:48:35.000Z"},{"title":"","url":"https://kiko.io/downloads/code/test.js","date":"2023-12-12T14:54:30.295Z"}]}
\ No newline at end of file
diff --git a/notes/2022/Morning-Routine-and-IndieWeb/index.html b/notes/2022/Morning-Routine-and-IndieWeb/index.html
index e380608b0a..ca0d8c0191 100644
--- a/notes/2022/Morning-Routine-and-IndieWeb/index.html
+++ b/notes/2022/Morning-Routine-and-IndieWeb/index.html
@@ -337,7 +337,7 @@
-
Get up. Make coffee. Grab Smartphone. Read news. Read RSS feeds. It goes like this or something similar every morning and I ALWAYS get stuck on the topic of IndieWeb. Surfing around for an hour or so. The topic simply hooked me. Today it was the wonderful work from Ryan Barrett (@snarfed.org@snarfed.org). Ended up thinking what I can do with …
+
Get up. Make coffee. Grab Smartphone. Read news. Read RSS feeds. It goes like this or something similar every morning and I ALWAYS get stuck on the topic of IndieWeb. Surfing around for an hour or so. The topic simply hooked me. Today it was the wonderful work from Ryan Barrett (@snarfed.org@snarfed.org). Ended up thinking what I can do with …
Get up. Make coffee. Grab Smartphone. Read news. Read RSS feeds. It goes like this or something similar every morning and I ALWAYS get stuck on the topic of IndieWeb. Surfing around for an hour or so. The topic simply hooked me. Today it was the wonderful work from Ryan Barrett (@snarfed.org@snarfed.org). Ended up thinking what I can do with …
+
Get up. Make coffee. Grab Smartphone. Read news. Read RSS feeds. It goes like this or something similar every morning and I ALWAYS get stuck on the topic of IndieWeb. Surfing around for an hour or so. The topic simply hooked me. Today it was the wonderful work from Ryan Barrett (@snarfed.org@snarfed.org). Ended up thinking what I can do with …
Ever had a fancy calculation written in C# and now you need it in JavaScript? Blazor WebAssembly has been developed to build such bridges and with .NET 7 it is very easy to externalize individual functions. With some overhead, but never mind. Thanks for this @meziantou@hachyderm.io …
+
Ever had a fancy calculation written in C# and now you need it in JavaScript? Blazor WebAssembly has been developed to build such bridges and with .NET 7 it is very easy to externalize individual functions. With some overhead, but never mind. Thanks for this @meziantou@hachyderm.io …
Ever had a fancy calculation written in C# and now you need it in JavaScript? Blazor WebAssembly has been developed to build such bridges and with .NET 7 it is very easy to externalize individual functions. With some overhead, but never mind. Thanks for this @meziantou@hachyderm.io …
+
Ever had a fancy calculation written in C# and now you need it in JavaScript? Blazor WebAssembly has been developed to build such bridges and with .NET 7 it is very easy to externalize individual functions. With some overhead, but never mind. Thanks for this @meziantou@hachyderm.io …
Ever had a fancy calculation written in C# and now you need it in JavaScript? Blazor WebAssembly has been developed to build such bridges and with .NET 7 it is very easy to externalize individual functions. With some overhead, but never mind. Thanks for this @meziantou@hachyderm.io …
+
Ever had a fancy calculation written in C# and now you need it in JavaScript? Blazor WebAssembly has been developed to build such bridges and with .NET 7 it is very easy to externalize individual functions. With some overhead, but never mind. Thanks for this @meziantou@hachyderm.io …
The sunsets off Saint-Malo are wonderful. The water of the English Channel glows and in the sky the silhuettes of the paragliders stand out, pulling the sportsmen over the rippling water. You can sit on the waterfront for hours and still not get enough of it.
-
+
@@ -369,7 +369,7 @@
Breton Sundown
Having good and clear weather here, as in the British Isles, is not a given. The evenings are all the more beautiful when it is like this and the small islands off the coast are bathed in a warm orange.
-
+
@@ -404,7 +404,7 @@
Breton Color
I love colors, but rarely does the camera manage to capture that magic the way I feel it. And Brittany has beautiful colors. Be it the famous Mont Saint-Michel or a cornfield … :)
-
+
@@ -439,7 +439,7 @@
Breton Sea Circle
Oysters from Cancale are not only famous in the region, but far beyond. A product that tastes just like the sea from which it comes looks: Awesome.
-
+
@@ -474,7 +474,7 @@
Breton Beach
The beach in Saint-Malo is 3 kilometers long and it is fascinating to observe how one of the highest tides in Europe swallows it in no time and later releases it cleaned.
+ data-id="clq2grrau0015nwpxcplne5qr" data-title="Checker Plus - Gmail in better..." />
-
+
This article appeared first on golem.de
on 20 Dec 2021
in German under the title «Checker Plus - Gmail in besser»
diff --git a/post/Contribute-with-Conventional-Commits/index.html b/post/Contribute-with-Conventional-Commits/index.html
index adbc3bc6c2..3177fad57b 100644
--- a/post/Contribute-with-Conventional-Commits/index.html
+++ b/post/Contribute-with-Conventional-Commits/index.html
@@ -386,15 +386,15 @@
The sea in Croatia is amazing clear because almost the entire coast is made of stone instead of sand. There is hardly any algae or other things that cloud the wonderful green-blue water.
-
+
@@ -371,7 +371,7 @@
Croatian Brightness
Yes, I’m guilty: I love colors and contrast! If you do also, his preset is for you. It brings out any image that is too flat or dull by helping to control the brightness and contrast.
-
+
@@ -406,7 +406,7 @@
Croatian Winter Drama
A drive along the coast in the morning or evening hours makes you want to stop all the time because one panorama is more beautiful than the next.
-
+
@@ -441,7 +441,7 @@
Croatian Winter City
The walls of Split have this wonderful warm tone of the winter sun and this preset brings this out particularly well.
-
+
@@ -476,7 +476,7 @@
Croatian Winter Sunset
The sunset in Croatia is special for me, because I feel these wonderful colors and my camera isn’t able to reproduce this feeling. This preset is an approximation of it.
The walls of Split have this wonderful warm tone of the winter sun and this preset brings this out particularly well.
This article appeared first on golem.de
on 11 Aug 2022
in German under the title «Ein Musikplayer, wie er sein sollte»
diff --git a/post/Experimenting-with-the-font-LEXEND/index.html b/post/Experimenting-with-the-font-LEXEND/index.html
index 2e84a28653..f9d5084849 100644
--- a/post/Experimenting-with-the-font-LEXEND/index.html
+++ b/post/Experimenting-with-the-font-LEXEND/index.html
@@ -417,15 +417,15 @@
+ data-id="clq2grrb1001fnwpxd3w7hq2l" data-title="Gitpod - Visual Studio Code on the Web" />
-
+
This article appeared first on golem.de
on 05 Jul 2022
in German under the title «Visual Studio Code im Web mit Gitpod - Ein Gewinn für jede Tool-Sammlung»
diff --git a/post/Handling-IPTC-metadata-on-Android-and-Windows/index.html b/post/Handling-IPTC-metadata-on-Android-and-Windows/index.html
index 5a89c3e7b6..e2635c3f0e 100644
--- a/post/Handling-IPTC-metadata-on-Android-and-Windows/index.html
+++ b/post/Handling-IPTC-metadata-on-Android-and-Windows/index.html
@@ -386,15 +386,15 @@
The mediaval walls of Jaffa glow in an inimitable way and brings other colors to shine the same way.
-
+
@@ -370,7 +370,7 @@
Israeli Lights
The light in the eastern Mediterranean is stunning. The warm tone of the sand and the turquoise color of the water had to pop out.
-
+
@@ -405,7 +405,7 @@
Israeli Drama
A visit of Yad Vashem moved me a lot and this preset is a expression of that.
-
+
@@ -440,7 +440,7 @@
Israeli Near Black
If you think of the tourists away, Jerusalem takes you to another level because of its age and history and nothing fits more to that than the sepia look of old pictures.
Usually the best photos end up unnoticed as pool photos here on my blog until I use one as a header image, but now I make a separate post out of each set before I publish the individual photos on other sites like 500px or Pixelfed.
It is possible that another aspect of the “island” image will apply to you: there is a local timeline on the islands, which is also called that. So, if you choose an island that is particularly suited to a geographical region, a topic or a language, then you will see the contributions of the other “inhabitants of this island” in their local timeline - which hopefully have a certain thematic cohesion. However, as already mentioned, you can follow all people on all instances and you will also see them in your own timeline.
Speaking of timelines: they are chronological and you only see the posts (toots) of those you follow. No algorithm, no “others like this too”, no good morning tweets in the late evening. Just the way Twitter used to be 10 years ago.
Follow: here is one of the points that bothers me the most, even if it is a small thing. Currently there are two software versions of Mastodon in use: 3.5.3 and four, which seem to handle following a bit differently.
-
In one (I assume the older version), if you want to follow an account on another instance, you have to specify your own handle again (in my case “@pjakobs@mastodon.green“ - but that’s no longer the case in the new version, there you click a follow button and you follow the account.
+
In one (I assume the older version), if you want to follow an account on another instance, you have to specify your own handle again (in my case “@pjakobs@mastodon.green“ - but that’s no longer the case in the new version, there you click a follow button and you follow the account.
Another thing that is very very different is DMs. Here on Twitter, we’re used to DMs essentially working like little emails or a messenger: we have our own inbox and that’s where the dialogue with the other person shows up. Feels good, not visible to others, because yes, in a completely different realm. In Mastodon, direct messages are simply toots that are displayed only to the recipient(s) - and normally among all the other interactions.
It’s confusing and doesn’t feel as nicely protected as it does here, and as I walked around town a bit yesterday I realized: this is intentional. Twitter creates an apparent privacy through display that DMs don’t offer. DMs are deliberately not PM, not private messages. Mastodon also makes that clear through its presentation. I have therefore added my Threema and Telegram link to my profile, these tools actually offer private conversations. Is not comfortable but honest.
You have created an account, but do not want to move 100% yet or, as in my case, still have many friends on this site? There is also a kind of “bridge” from Mastodon archipelago to Twitter. You can mirror your tweets to Mastodon or vice versa. This is only valid for tweets, not for replies. That means you have to be a bit careful that you don’t just spam your followers in the other network, but also interact with them. After a few days of mirroring from Twitter to Mastodon I turned the bridge around, today probably 80% of the tweets you see from me and the non-replies are on Mastodon. I use crossposter.masto.donte.com.br for this.
Not only because it felt like luxury camping without people but only water around, but also because it gave a pleasant feeling of freedom. Yes, we weren’t nearly alone on the lakes and getting a good spot in a marina for electricity and fresh water wasn’t always easy, but then just anchoring in any lake for the night and being woken up by birdsong and a gentle swell was magical.
Below are a few pictures from this trip, some of which will be used again as hero images in this blog:
+ data-id="clq2grrb3001jnwpxebmhgqm3" data-title="One mouse to rule them all" />
-
+
This article appeared first on golem.de
on 30 Sep 2022
in German under the title «Eine Maus für alles»
diff --git a/post/Open-Source-Insights-Seeing-the-big-picture/index.html b/post/Open-Source-Insights-Seeing-the-big-picture/index.html
index befd3c0271..2e0de7e62e 100644
--- a/post/Open-Source-Insights-Seeing-the-big-picture/index.html
+++ b/post/Open-Source-Insights-Seeing-the-big-picture/index.html
@@ -389,15 +389,15 @@
Jonathan Puckey and his studio played a key role in the project and its technical implementation. In several iteration stages, this resulted in the web app radio.garden, which Puckey is still in charge of today. Instead of maps, the team from Amsterdam used satellite images from the beginning to illustrate that radio signals have always had the power to cross borders.
Radio Garden is free, and you don’t need to register, log in, or even provide an email address. Nothing. Open up, select a station, listen to the radio.
-
+
-
+
This article appeared first on golem.de
on 26 Jan 2023
in German under the title «Alle Arten kommen in den Garten»
diff --git a/post/Remote-Testing-and-Debugging-with-Chrome/index.html b/post/Remote-Testing-and-Debugging-with-Chrome/index.html
index 8e224138e2..b2bfa9514c 100644
--- a/post/Remote-Testing-and-Debugging-with-Chrome/index.html
+++ b/post/Remote-Testing-and-Debugging-with-Chrome/index.html
@@ -436,15 +436,15 @@
The first 10 minutes of the game were unspectacular. They felt each other out and the Finns played diligently and should have been in the lead after 5 minutes, but got in the 9th minute after some guesswork from the referee with VAR support a penalty against them.
The coach seemed to have had something similar in mind, because as soon as the ball started rolling, our boys attacked their opponents and had their first 100% chance after just 20 seconds (!). A few minutes later, the ball was actually in the goal, but it was probably disallowed by the referee for offside or something similar. I don’t know but it didn’t matter as long as it continued at this pace. Braunschweig were hopelessly out of their depth and simply tried to prevent the inevitable … until the 18th minute … 1:0 - A fine header by our defender Aleksandar Vukotic :)
However, the problem was that our team then let themselves go a little and only benefited from the opponent’s harmlessness until the break. As if the job was already done…
HSV is actually in shape this year to make it back to the Bundesliga. They have already dropped a few points, but have been on the promotion places from the beginning. This year it should be something and accordingly dominant have the Hamburg the game against once again deep standing Wiesbadener also started. Very sure of the ball and winning almost every duel, they were unable to capitalize on this in the first half. They brought the ball very close to the goal, but not into it, and you could tell that the guests were getting a little more annoyed as time went on.
This was expressed shortly before the half-time break also in a few unsportsmanlike conduct. Dropping and claiming foul play is simply stupid and only provokes catcalls. The one or other time is overall bad referee but also fell for the trick or has made other nonsensical decisions, which caused the “right” fans around me to nastiest insults.
Stritzel has saved fantastic balls this season, but not a penalty yet … until then, as he parried Kinsombi’s weak shot to the side. Yesss…!
Nothing much happened until half-time, but our guys were slowly gaining the upper hand, as the game was played almost exclusively in the opponent’s half at the beginning of the second half, with occasional counter-attacks by Rostock, but they were too erratic to have any effect. Our defence stood firm.
In the first half, the game was rather well balanced, even if you had the feeling that our team was the only ones playing. There was no sign of Kaiserlautern’s attacking drive. Up until the 39th minute, we had one chance to score, but the visitors hadn’t even had a shot on our goal. Nothing, nada, niente. But then the ball was in our net: 0:1! A strange goal, because it didn’t look intentional. The ball just bounced at Ritter’s feet by chance and he simply took a shot. Goal. Damn…
The club has of course strengthened itself for the new season with new players in all sections of the team, whom I have now seen play for the first time. And unfortunately, some of them, such as Brooklyn Ezeh and Benedikt Hollerbach, who grew on me during the few games last year, have moved to other clubs.
It wasn’t, however, that Schalke dominated the game. We carelessly let them just do it and the fans had their fun when they shot the ball into the clouds again after a lot of roaring and stomping. Funny.
At the beginning of the second half, this continued seamlessly, even if you noticed that the coach in the cabin must have yelled at one or the other and these were now somewhat more active … and suddenly the ball was in our net … 0:1! What? How? A goal in the 54. minute for Schalke out of (almost) nothing! I have to watch that again on TV … :|
When it gets late in the city, the artificial light bathes them in a warm veil. The shadows are deeper and the colors are more concise. Time to get a drink.
-
+
@@ -368,7 +368,7 @@
Scandinavian Blue Hour
All over the world, the Blue Hour is a special time of the day. The light is fading away slightly and everything shines in magic colors. Let them shine…
-
+
@@ -403,7 +403,7 @@
Scandinavian Colors
Nordic nature is far from being as rich and colorful as that in the south, but it has its own charm and with color you can help a little.
-
+
@@ -438,7 +438,7 @@
Scandinavian Drama
Not only since Shakespeare and his Hamlet, we know about the dramas of the Nordic sagas. The landscape itself is dramatic and the stories are set in it.
-
+
@@ -473,7 +473,7 @@
Scandinavian Seascape
The sea plays a big role in the Nordic countries, as they have very long coasts. No one who travels there with a camera manages to resist this beauty.
So far in the north the light is totally different. When it shines through the clouds, it hits you.
-
+
@@ -368,7 +368,7 @@
Scotch Strength
You must be made of different stuff if you are Scottish. There is hardly anything fine and graceful about the landscape and the weather. It’s rough and tough and you have to deal with it.
-
+
@@ -403,7 +403,7 @@
Scotch Sunset
The view over the North Sea at the edge of the continent is really unique, especially at sunset. Hear the the seagulls scream, the waves crashing against the cliffs and see the stunning colors.
-
+
@@ -438,7 +438,7 @@
Scotch Energy
Scotland is bold and so full with energy, even in the details.
-
+
@@ -473,7 +473,7 @@
Scotch Tattoo
The Edinburgh Military Tattoo, which takes place twice a year, is a feast for the senses. The incredible sound on one hand and the colors on the other. The brass, the feathers, the uniforms stand out in the setting sun.
These images are absolute great for background images in websites, in order to make the details less recognisable.
There is a web version of Triangula, but the desktop version (including a console version) is much faster. Best feature is the ability not only to save the generated images as PNG, but also as SVG!
This article appeared first on golem.de
on 29 Nov 2021
in German under the title «Tringula und die Schönheit der Mathematik»
diff --git a/post/Use-a-duplicate-image-to-drop-a-shadow/index.html b/post/Use-a-duplicate-image-to-drop-a-shadow/index.html
index 42f3ab850d..3662522aff 100644
--- a/post/Use-a-duplicate-image-to-drop-a-shadow/index.html
+++ b/post/Use-a-duplicate-image-to-drop-a-shadow/index.html
@@ -409,15 +409,15 @@
In the meantime we were on two more concerts in Frankfurt, then in Marburg (Open-Air) and in Mainz on the “Night of the Ballads”. Most recently we saw them two weeks ago at the MPS (“Mittelalterlich Phantasie Spectaculum”, a medieval festival) in Speyer, where they played open-air with other bands from the “industry”. On the one hand, I think it’s great that the guys, even if they now fill large halls in Germany, have not forgotten their origins, and on the other hand, I now had the opportunity to get very close to the stage with my camera.
+ data-id="clq2grrba0025nwpxgr4wantf" data-title="Generate Social Media Images Automatically" />
diff --git a/rss-excerpt.xml b/rss-excerpt.xml
index 0e1b64d730..e2ff0a8c48 100644
--- a/rss-excerpt.xml
+++ b/rss-excerpt.xml
@@ -16,10 +16,10 @@
HexoGitHubBlogging
- jQueryCSSStylusDark Mode
+ jQueryADSIVisual StudioDatabase Project
diff --git a/rss.xml b/rss.xml
index fb707ac66b..3993894220 100644
--- a/rss.xml
+++ b/rss.xml
@@ -16,10 +16,10 @@
HexoGitHubBlogging
- jQueryCSSStylusDark Mode
+ jQueryADSIVisual StudioDatabase Project
@@ -110,12 +110,12 @@
<h2 id="The-Game"><a href="#The-Game" class="headerlink" title="The Game"></a>The Game</h2><p>The coach seemed to have had something similar in mind, because as soon as the ball started rolling, our boys attacked their opponents and had their first 100% chance after just 20 seconds (!). A few minutes later, the ball was actually in the goal, but it was probably disallowed by the referee for offside or something similar. I don’t know but it didn’t matter as long as it continued at this pace. Braunschweig were hopelessly out of their depth and simply tried to prevent the inevitable … until the 18th minute … 1:0 - A fine header by our defender <strong>Aleksandar Vukotic</strong> :)</p>
<p>However, the problem was that our team then let themselves go a little and only benefited from the opponent’s harmlessness until the break. As if the job was already done…</p>
- <div class="image-masonry" id="image-masonry-n3hseo">
+ <div class="image-masonry" id="image-masonry-k5yygd">
<div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_165816147.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_170229560.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_170946775.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_172204474.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_172706390.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_173932826.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_174727767.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_183243917.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Braunschweig-2023-12-08/PXL_20231208_184445039.jpg" alt="" /></div>
</div>
<script>
- let macy_n3hseo = new Macy({
- container: '#image-masonry-n3hseo',
+ let macy_k5yygd = new Macy({
+ container: '#image-masonry-k5yygd',
trueOrder: false,
waitForImages: false,
useOwnImageLoader: false,
@@ -217,7 +217,7 @@
<p>Not only because it felt like luxury camping without people but only water around, but also because it gave a pleasant feeling of freedom. Yes, we weren’t nearly alone on the lakes and getting a good spot in a marina for electricity and fresh water wasn’t always easy, but then just anchoring in any lake for the night and being woken up by birdsong and a gentle swell was magical.</p>
<p>Below are a few pictures from this trip, some of which will be used again as hero images in this blog:</p>
- <div class="photo-list" id="photo-list-d1g0gm">
+ <div class="photo-list" id="photo-list-16sdnb">
<figure>
<a href="/photos/23-08-Mecklenburg-Seen-0040" class="no-break">
@@ -526,7 +526,7 @@
<span id="more"></span>
- <div class="photo-list" id="photo-list-f9l40b">
+ <div class="photo-list" id="photo-list-2cn8v9">
<figure>
<a href="/photos/23-09-11-Speyer-0001" class="no-break">
@@ -622,12 +622,12 @@
<hr>
<h2 id="The-Game"><a href="#The-Game" class="headerlink" title="The Game"></a>The Game</h2><p>In the first half, the game was rather well balanced, even if you had the feeling that our team was the only ones playing. There was no sign of Kaiserlautern’s attacking drive. Up until the 39th minute, we had one chance to score, but the visitors hadn’t even had a shot on our goal. Nothing, nada, niente. But then the ball was in our net: <strong>0:1</strong>! A strange goal, because it didn’t look intentional. The ball just bounced at Ritter’s feet by chance and he simply took a shot. Goal. Damn…</p>
- <div class="image-masonry" id="image-masonry-em1qnj">
+ <div class="image-masonry" id="image-masonry-odjtmi">
<div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_120526904.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_122110857.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_122854839.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_122739398.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_122947255.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_135210240.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_141045409.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_142300311.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_142353462.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_142646533.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_142730319.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Kaiserslautern-2023-11-12/PXL_20231112_142841643.jpg" alt="" /></div>
</div>
<script>
- let macy_em1qnj = new Macy({
- container: '#image-masonry-em1qnj',
+ let macy_odjtmi = new Macy({
+ container: '#image-masonry-odjtmi',
trueOrder: false,
waitForImages: false,
useOwnImageLoader: false,
@@ -811,12 +811,12 @@
<p>Stritzel has saved fantastic balls this season, but not a penalty yet … until then, as he parried Kinsombi’s weak shot to the side. Yesss…!</p>
<p>Nothing much happened until half-time, but our guys were slowly gaining the upper hand, as the game was played almost exclusively in the opponent’s half at the beginning of the second half, with occasional counter-attacks by Rostock, but they were too erratic to have any effect. Our defence stood firm.</p>
- <div class="image-masonry" id="image-masonry-1o3k41">
+ <div class="image-masonry" id="image-masonry-empr3t">
<div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_122726572.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_122837500.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_121402556.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_123926894.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_140540817.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_141750690.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_142643939.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_142901228.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Hansa-Rostock-2023-10-29/PXL_20231029_142951071.jpg" alt="" /></div>
</div>
<script>
- let macy_1o3k41 = new Macy({
- container: '#image-masonry-1o3k41',
+ let macy_empr3t = new Macy({
+ container: '#image-masonry-empr3t',
trueOrder: false,
waitForImages: false,
useOwnImageLoader: false,
@@ -882,12 +882,12 @@
<hr>
<h2 id="The-Game"><a href="#The-Game" class="headerlink" title="The Game"></a>The Game</h2><p>The first 10 minutes of the game were unspectacular. They felt each other out and the Finns played diligently and should have been in the lead after 5 minutes, but got in the 9th minute after some guesswork from the referee with VAR support a penalty against them. </p>
- <div class="image-masonry" id="image-masonry-oz7qh4">
+ <div class="image-masonry" id="image-masonry-ydg4i0">
<div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_191040789.jpg" alt="" /></div><div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_191000090.jpg" alt="" /></div>
</div>
<script>
- let macy_oz7qh4 = new Macy({
- container: '#image-masonry-oz7qh4',
+ let macy_ydg4i0 = new Macy({
+ container: '#image-masonry-ydg4i0',
trueOrder: false,
waitForImages: false,
useOwnImageLoader: false,
@@ -923,12 +923,12 @@
<p>In the break, the coach of the Finns seems to have been a little louder, because his guys played a little stronger and more determined, only to get another one in the 55th minute.</p>
<p>With 10 minutes to go, things got emotional in the stadium: the coach substituted Timothy Chandler, a Frankfurt veteran who has been with the club for almost 10 years and his first appearance this season. The tens of thousands of fans were completely out of their minds, screaming “Tiiiimmmyyy” as soon as he got to the ball. It was crazy. How must it feel to be showered with so much fan love! Things almost got out of hand when Chandler sprinted forward on the right flank in the 89th minute and made a wonderful pass into the middle and Dina Ebimbe just had to slot it in for 6:0.</p>
- <div class="image-masonry" id="image-masonry-whk9da">
+ <div class="image-masonry" id="image-masonry-2l3use">
<div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_183650025.jpg" alt="Attila, the Eintracht Eagle" /></div><div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_184409439.jpg" alt="Game Comic" /></div><div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_183658672.jpg" alt="Just before kickoff" /></div><div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_192703385.jpg" alt="2:0" /></div><div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_193136300.jpg" alt="Kickoff after 3:0" /></div><div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_205110961.jpg" alt="Final whistle" /></div><div><img class="no-caption" src="/post/SGE-vs-HJK-2023-10-26/PXL_20231026_205742073.jpg" alt="Lap of Honor" /></div>
</div>
<script>
- let macy_whk9da = new Macy({
- container: '#image-masonry-whk9da',
+ let macy_2l3use = new Macy({
+ container: '#image-masonry-2l3use',
trueOrder: false,
waitForImages: false,
useOwnImageLoader: false,
@@ -1084,12 +1084,12 @@
<h2 id="The-Game"><a href="#The-Game" class="headerlink" title="The Game"></a>The Game</h2><p>HSV is actually in shape this year to make it back to the Bundesliga. They have already dropped a few points, but have been on the promotion places from the beginning. This year it should be something and accordingly dominant have the Hamburg the game against once again deep standing Wiesbadener also started. Very sure of the ball and winning almost every duel, they were unable to capitalize on this in the first half. They brought the ball very close to the goal, but not into it, and you could tell that the guests were getting a little more annoyed as time went on.</p>
<p>This was expressed shortly before the half-time break also in a few unsportsmanlike conduct. Dropping and claiming foul play is simply stupid and only provokes catcalls. The one or other time is overall bad referee but also fell for the trick or has made other nonsensical decisions, which caused the “right” fans around me to nastiest insults.</p>
- <div class="image-masonry" id="image-masonry-zs6k4c">
+ <div class="image-masonry" id="image-masonry-9to8wg">
<div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_103936756.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_104935878.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_105740138.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_105912858.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_110002467.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_110827714.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_111728897.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_123948926.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_125719562.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-HSV-2023-10-07/PXL_20231007_125751495.jpg" alt="" /></div>
</div>
<script>
- let macy_zs6k4c = new Macy({
- container: '#image-masonry-zs6k4c',
+ let macy_9to8wg = new Macy({
+ container: '#image-masonry-9to8wg',
trueOrder: false,
waitForImages: false,
useOwnImageLoader: false,
@@ -1143,7 +1143,7 @@
<p>Usually the best photos end up unnoticed as <a href="/post/Pool-Photo-Generator/" title="Pool Photo Generator">pool photos</a> here on my blog until I use one as a header image, but now I make a separate post out of each set before I publish the individual photos on other sites like <a href="https://500px.com/p/kikon">500px</a> or <a href="https://pixelfed.social/kristofz">Pixelfed</a>.</p>
- <div class="photo-list" id="photo-list-med46o">
+ <div class="photo-list" id="photo-list-1gg7tl">
<figure>
<a href="/photos/23-07-Mallorca-0300" class="no-break">
@@ -1459,12 +1459,12 @@
<p>In the meantime we were on two more concerts in Frankfurt, then in Marburg (Open-Air) and in Mainz on the “Night of the Ballads”. Most recently we saw them two weeks ago at the MPS (“Mittelalterlich Phantasie Spectaculum”, a medieval festival) in Speyer, where they played open-air with other bands from the “industry”. On the one hand, I think it’s great that the guys, even if they now fill large halls in Germany, have not forgotten their origins, and on the other hand, I now had the opportunity to get very close to the stage with my camera.</p>
<p>Here are the results:</p>
- <div class="image-masonry" id="image-masonry-j3et5x">
+ <div class="image-masonry" id="image-masonry-ferwmb">
<div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0092.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0094.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0103.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0104.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0105.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0106.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0108.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0111.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0115.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0123.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0126.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0128.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0131.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0136.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0150.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0151.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0152.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0153.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0156.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0157.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0159.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0162.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0169.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0171.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0172.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0173.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0177.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0179.jpg" alt="" /></div><div><img class="no-caption" src="/post/Versengold-in-Concert/23-08-26-MPS-Speyer-0182.jpg" alt="" /></div>
</div>
<script>
- let macy_j3et5x = new Macy({
- container: '#image-masonry-j3et5x',
+ let macy_ferwmb = new Macy({
+ container: '#image-masonry-ferwmb',
trueOrder: false,
waitForImages: false,
useOwnImageLoader: false,
@@ -1521,12 +1521,12 @@
<p>It wasn’t, however, that Schalke dominated the game. We carelessly let them just do it and the fans had their fun when they shot the ball into the clouds again after a lot of roaring and stomping. Funny.</p>
<p>At the beginning of the second half, this continued seamlessly, even if you noticed that the coach in the cabin must have yelled at one or the other and these were now somewhat more active … and suddenly the ball was in our net … <strong>0:1</strong>! What? How? A goal in the 54. minute for Schalke out of (almost) nothing! I have to watch that again on TV … :|</p>
- <div class="image-masonry" id="image-masonry-694dqk">
+ <div class="image-masonry" id="image-masonry-25bgg5">
<div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_104957617.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_105327820.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_105347190.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_105548827.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_105749100.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_124411594.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_111931000.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_120730081.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_125820397.jpg" alt="" /></div><div><img class="no-caption" src="/post/SVWW-vs-Schalke-2023-09-02/PXL_20230902_130050774.jpg" alt="" /></div>
</div>
<script>
- let macy_694dqk = new Macy({
- container: '#image-masonry-694dqk',
+ let macy_25bgg5 = new Macy({
+ container: '#image-masonry-25bgg5',
trueOrder: false,
waitForImages: false,
useOwnImageLoader: false,
@@ -1592,12 +1592,12 @@
<p><strong>Live Output:</strong></p>
- <div class="image-masonry" id="image-masonry-k0yq6b">
+ <div class="image-masonry" id="image-masonry-i7ci44">
<div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/D50_0053.jpg" alt="Thomas' Ruby Prince I" /></div><div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/_D50_3251.jpg" alt="No Name" /></div><div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/D50_0086.jpg" alt="Thomas' German Flag" /></div><div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/D50_1147.jpg" alt="Poppy Green" /></div><div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/D50_0075.jpg" alt="Thomas Wild Tulips" /></div><div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/D50_7474.jpg" alt="Garden Beauties XIV" /></div><div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/D50_4451.jpg" alt="Garden Beauties I" /></div><div><img class="no-caption" src="/post/Image-Masonry-Tag-Plugin-for-Hexo/../../photos/normal/D50_1577.jpg" alt="Floral Magic XIV" /></div>
</div>
<script>
- let macy_k0yq6b = new Macy({
- container: '#image-masonry-k0yq6b',
+ let macy_i7ci44 = new Macy({
+ container: '#image-masonry-i7ci44',
trueOrder: false,
waitForImages: false,
useOwnImageLoader: false,
diff --git a/series/a-new-blog/index.html b/series/a-new-blog/index.html
index a0aa05636c..7ff3dd4e8b 100644
--- a/series/a-new-blog/index.html
+++ b/series/a-new-blog/index.html
@@ -363,15 +363,15 @@