Posts
Celebrating World Emoji Day my way
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mr3uuuabsc2i",
"data": {
"path": "/3mr3uuuabsc2i",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mjjki3asbs2k",
"tags": [
"emoji"
],
"$type": "site.standard.document",
"title": "Celebrating World Emoji Day my way",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f7e11-983d-7998-b072-877df0f9679c",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "A variety of colorful emojis displayed on a white background, showcasing different facial expressions and emotions. ",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreig6pdut24uv2fz4sm43i5ykvptpxwxxsyketygbedy7ko7a6k4r3e"
},
"size": 271760,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2400,
"height": 1577
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 125,
"byteStart": 118
},
"features": [
{
"uri": "https://bsky.app/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 145,
"byteStart": 137
},
"features": [
{
"uri": "https://npmx.dev",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "I really like emojis. You might have noticed this last Friday, the 17th of July, 2026, if you were a little active on BlueSky or visited npmx.dev."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Art"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 76,
"byteStart": 67
},
"features": [
{
"did": "did:plc:u5zp7npt5kpueado77kuihyz",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
},
{
"index": {
"byteEnd": 245,
"byteStart": 226
},
"features": [
{
"uri": "https://www.doodles.google/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 425,
"byteStart": 410
},
"features": [
{
"uri": "https://worldemojiday.com/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 516,
"byteStart": 515
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f7e1b-1ebd-7112-8bef-a8d3eb55cf4e",
"contentFacets": [
{
"index": {
"byteEnd": 42,
"byteStart": 0
},
"features": [
{
"uri": "https://en.wikipedia.org/wiki/Felix_(name)",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"contentPlaintext": "https://en.wikipedia.org/wiki/Felix_(name)"
}
]
}
],
"plaintext": "Around two weeks ago, a random idea hit me: I wanted to create the @npmx.dev logo just with emojis. This made me realize that it would be a perfect fit for a so-called \"noodle\" in npmx. It is our way of continuing the beloved Doodles from Google. But as you might know, Doodles have meaning bound to dates. Just for the sake of it, I prompted for the date that would be most associated with emojis. Turns out, World Emoji Day was right around the corner. Well, Felix means βthe lucky oneβ in Latin for a reason.*"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 56,
"byteStart": 46
},
"features": [
{
"did": "did:plc:z7mxys3ms5slfb26r5vmtve2",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
}
],
"plaintext": "Hooked by the idea, I immediately jumped into @figma.com to create the noodle, not knowing that it would take me over 4h to copy-paste enough emojis, sort them by color and position them to form the letters \"npmx\"."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "First draft of the npmx logo consisting of very small emojis, each letter focused on one emoji color.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreia3wnu3dae3xhyx7pqvbhyitlsnp6mn4kidwbl52if3rlh5ku46ju"
},
"size": 142178,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 1859,
"height": 1230
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 68,
"byteStart": 56
},
"features": [
{
"uri": "https://build.npmx.dev",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 152,
"byteStart": 129
},
"features": [
{
"did": "did:plc:ixybdvuij65nvgnoio53qyiq",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
}
],
"plaintext": "But the effort was not in vain. After sharing it on the npmx Discord, people really liked it. And they had some good ideas. Like @graphieros.npmx.social, who suggested that it would be fun if some emojis would randomly blink or swap."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 19,
"byteStart": 17
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
},
{
"index": {
"byteEnd": 80,
"byteStart": 78
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
},
{
"index": {
"byteEnd": 102,
"byteStart": 101
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f7e36-1c8e-7555-92bc-fa222bb67702",
"contentFacets": [
{
"index": {
"byteEnd": 36,
"byteStart": 0
},
"features": [
{
"uri": "https://roe.dev/blog/virtuous-circle",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"contentPlaintext": "https://roe.dev/blog/virtuous-circle"
}
]
}
],
"plaintext": "While this threw my already tight schedule into disarray, it didn't mean that we couldn't achieve it.* And oh boy, the dedication of one special community member really had us all on the edge of our seats."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 14,
"byteStart": 0
},
"features": [
{
"did": "did:plc:vmiebbb4cxmbnq5giqqhlvkj",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
}
],
"plaintext": "@jviide.iki.fi went completely over the top and dropped this bomb straight in the midst of our #noodle channel:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "A fun, chaotic explosion of floating emojis on a dark backgroundβfeaturing everything from smiley faces to random animals and foodβthat come together to form the npmx logo.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreiexsagokwjjfq6at42xdmqn32eceq2mp6bm34xwtsnqdg5h3br2fq"
},
"size": 51117821,
"$type": "blob",
"mimeType": "image/gif"
},
"aspectRatio": {
"width": 1980,
"height": 928
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "What an absolute legend!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 104,
"byteStart": 84
},
"features": [
{
"uri": "https://npmx.dev/noodles/emoji-day",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "They hooked the entire prototype into the npmx landing page and created a wonderful noodle archive entry. Therefore, you can now enjoy our wonderful creation for the rest of the day or your life - depending on your dedication. Speaking of which..."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Dedication"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "At the same time, I was planning a little fun challenge for the 17th of July: Only post emojis on BlueSky. No links. No visual text."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 25,
"byteStart": 0
},
"features": [
{
"did": "did:plc:o25mmymawqpsq77rzkw64cva",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
},
{
"index": {
"byteEnd": 50,
"byteStart": 27
},
"features": [
{
"did": "did:plc:ixybdvuij65nvgnoio53qyiq",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
},
{
"index": {
"byteEnd": 62,
"byteStart": 52
},
"features": [
{
"did": "did:plc:dfkjiu36xs6ogt7pux7i7o2b",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
},
{
"index": {
"byteEnd": 75,
"byteStart": 67
},
"features": [
{
"did": "did:plc:qfpnj4og54vl56wngdriaxug",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
}
],
"plaintext": "@lopeadebesin.bsky.social, @graphieros.npmx.social, @willow.sh and @oppi.li joined me and together we posted over 84 unique emojis. I myself might have overdone it a little bit as I published 16 quotes, one post, three reposts and 35 replies that day, sending a total of 127 grinning little faces, symbols and individually selected visually appealing Unicode characters over the Atmosphere. None of which included any letters by our strict rules."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The irony in all of this is that I have now written an entire post consisting almost entirely of letters and image bytes without a single emoji. One could say that after such a special day, I need to restock my emoji supply, which might take some time..."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.horizontalRule"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Ha! I doubt it! π\nHere are all 84 unique emojis we collected together that day: "
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "πΆπ―πππ€©ππΈπβ½οΈπ»π«Άπ¦π¬βΎοΈπππππ₯π€β
ππ°π€£π₯°β€οΈβοΈβοΈππ0οΈβ£π₯Ήπππ’π₯π₯β¨π₯²πΈπ°π₯³βοΈ1οΈβ£πβοΈβ’οΈπ€―πππ«π‘πππβοΈπ€ππποΈππ«‘ππππ»π€βπ§βπ»ππ
πππποΈπ€ππ§β€΄οΈπͺππ΄\n\nIn the end, there is nothing left to say, apart from:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.bskyPost",
"postRef": {
"cid": "bafyreihxquuj2bpjcafprtpvqfaygfhgb5vfzhbj5sln4gwwkluclknvlu",
"uri": "at://did:plc:3nqrhu5mthmias3zc4a2ovzj/app.bsky.feed.post/3mqta4yrd722c"
},
"clientHost": "bsky.app"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Thanks to everyone involved! Until next year."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreig6pdut24uv2fz4sm43i5ykvptpxwxxsyketygbedy7ko7a6k4r3e"
},
"size": 271760,
"$type": "blob",
"mimeType": "image/webp"
},
"bskyPostRef": {
"cid": "bafyreiajvrs6gzg3aretiadmatnipfiw22l3xuo35ermrixs3wqpwphihi",
"uri": "at://did:plc:pbjvqaziagcyv2vqodldn5op/app.bsky.feed.post/3mr3uuzgsvk2m",
"commit": {
"cid": "bafyreidifhqarl5eupo5dz2xgnqt7hl4sskiemn6osspjibdgq6aebgpvu",
"rev": "3mr3uuzo4ey2s"
},
"validationStatus": "valid"
},
"description": "A quick recap of the chaotic, emoji-filled adventures and community creations from World Emoji Day 2026!",
"publishedAt": "2026-07-20T18:02:19.809Z",
"publication": {
"url": "https://felix.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreifufbuxu7ci3whmdz7wf4lnar4gsacqngucuik3hwdhhjhn6uv7zm"
},
"size": 12676,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Felix",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 39,
"g": 39,
"r": 39,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 624,
"accentText": {
"b": 255,
"g": 255,
"r": 255,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 250,
"g": 252,
"r": 253,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"b": 252,
"g": 254,
"r": 255,
"$type": "pub.leaflet.theme.color#rgb"
},
"accentBackground": {
"b": 30,
"g": 130,
"r": 87,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 30,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 252,
"g": 254,
"r": 255,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Heartfelt Thanks & Credits
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mqaov5uzks2x",
"data": {
"path": "/3mqaov5uzks2x",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mjjki3asbs2k",
"tags": [
"credits",
"love",
"thank you"
],
"$type": "site.standard.document",
"title": "Heartfelt Thanks & Credits",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f328e-5892-7003-93dd-e56e42af23f3",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Two hands form a heart shape against a bright sunlit background, symbolizing love and warmth. ",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreihocwpxlnfmuadg6knc4asf2alzyhijqt65k5ouqg7tvkyvf43mc4"
},
"size": 127906,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2074,
"height": 1383
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "In this little entry, I just want to express my sincere thanks to all the people making my blogs possible. "
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 23,
"byteStart": 11
},
"features": [
{
"did": "did:plc:btxrwcaeyodrap5mnjw2fvmz",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
},
{
"index": {
"byteEnd": 109,
"byteStart": 95
},
"features": [
{
"did": "did:plc:re3ebnp5v7ffagz6rb6xfei4",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
},
{
"index": {
"byteEnd": 193,
"byteStart": 184
},
"features": [
{
"did": "did:plc:z72i7hdynmk6r22z27h6tvur",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
},
{
"index": {
"byteEnd": 227,
"byteStart": 215
},
"features": [
{
"did": "did:plc:ewvi7nxzyoun6zhxrhs64oiz",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
}
],
"plaintext": "Thank you, @leaflet.pub, for creating such an amazing publication platform for free. Thanks to @standard.site for making blogs on the Atmosphere standardized! Another big thank you to @bsky.app itself for inventing @atproto.com, the platform I fell in love with this year."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 28,
"byteStart": 18
},
"features": [
{
"did": "did:plc:revjuqmkvrw6fnkxppqtszpv",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
},
{
"index": {
"byteEnd": 46,
"byteStart": 33
},
"features": [
{
"did": "did:plc:pgjkomf37an4czloay5zeth6",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
}
],
"plaintext": "Shoutouts also to @pckt.blog and @offprint.app for wonderful publication sites. I love you all for your variety. Stay just the way you are!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 40,
"byteStart": 28
},
"features": [
{
"did": "did:plc:6kf6jxl44h34mprhykvqljcx",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
},
{
"index": {
"byteEnd": 68,
"byteStart": 56
},
"features": [
{
"uri": "https://github.com/withastro/starlight/graphs/contributors?all=1",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 81,
"byteStart": 72
},
"features": [
{
"uri": "https://starlight.astro.build/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 111,
"byteStart": 100
},
"features": [
{
"did": "did:plc:n2bc7yctwi2h4twqywulfl5s",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
},
{
"index": {
"byteEnd": 135,
"byteStart": 114
},
"features": [
{
"uri": "https://starlight-blog-docs.vercel.app/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Special thanks to the whole @astro.build community, all contributors to Starlight and especially to @hideoo.dev's Starlight Blog plugin, which powered my previous blog and motivated me to keep writing."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 68,
"byteStart": 55
},
"features": [
{
"did": "did:plc:zbbeyhrkv72q54m7ezfwombo",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
}
],
"plaintext": "A lot of hearts also to the amazing photographers from @unsplash.com, whose pictures add the perfect flair to every post. This section is dedicated to all of you!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 16,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@pablocontreras",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 26,
"byteStart": 21
},
"features": [
{
"uri": "https://unsplash.com/photos/brown-wooden-building-under-white-clouds-during-daytime-pdRpNwIJMGQ",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 89,
"byteStart": 35
},
"features": [
{
"uri": "https://yappin.leaflet.pub/3mq3fijas2s25",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "@invadingkingdom β Image used on Vibe-coding a VS Code Extension for Terraform projects"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 15,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@aedrian",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 25,
"byteStart": 20
},
"features": [
{
"uri": "https://unsplash.com/photos/a-black-background-with-lots-of-circles-and-bubbles-HBD5k7sA3MQ",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 56,
"byteStart": 34
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mpvwzmluok2z",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Aedrian Salazar β Image used on Recommended Git Config"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 13,
"byteStart": 0
},
"features": [
{
"uri": "https://animalia-life.club/qa/pictures/high-resolution-nasa-earth-at-night-from-space",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 23,
"byteStart": 18
},
"features": [
{
"uri": "https://ena.our-dogs.info/facts-ph.html",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 115,
"byteStart": 32
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mptsiqpfzs2o",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Animalia Life β Image used on Designing a Kafka-Based Pipeline and Interactive Graph for Energy Anomaly Detection"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 11,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@bilal_ayadi",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 21,
"byteStart": 16
},
"features": [
{
"uri": "https://unsplash.com/photos/yellow-and-green-lighted-candle-k8Lp1IOsZt4",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 88,
"byteStart": 30
},
"features": [
{
"uri": "https://yappin.leaflet.pub/3mq3cbav26s2f",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Bilal Ayadi β Image used on From Balloons to Binary: A 20th Birthday Packed with Clues"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 11,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@dlxmedia",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 21,
"byteStart": 16
},
"features": [
{
"uri": "https://unsplash.com/photos/a-pen-and-a-cup-of-coffee-CF5I-rfrJ4s",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 112,
"byteStart": 30
},
"features": [
{
"uri": "https://yappin.leaflet.pub/3mq3eqn2c4k2c",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "dlxmedia.hu β Image used on Some aspects of creating a blog are really easy, and some are extremely difficult!"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 14,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@miracleday",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 24,
"byteStart": 19
},
"features": [
{
"uri": "https://unsplash.com/photos/a-bulletin-board-with-a-back-to-school-sign-on-it-1nz2RyutBEE",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 83,
"byteStart": 33
},
"features": [
{
"uri": "https://yappin.leaflet.pub/3mq36e6nxrk2l",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Elena Mozhvilo β Image used on Organising School Life: The Journey Behind Mutanuq"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 14,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@e_clement",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 24,
"byteStart": 19
},
"features": [
{
"uri": "https://unsplash.com/photos/brown-and-white-cardboard-box-TgUwEK-RNUQ",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 92,
"byteStart": 33
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mpvisgc65k2f",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Evelyn Clement β Image used on How VS Code Snippets can improve your workflow in Starlight"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 5,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@fhavlik",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 15,
"byteStart": 10
},
"features": [
{
"uri": "https://unsplash.com/photos/black-and-white-candybar-phone-VHf4jqrUu7g",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 77,
"byteStart": 24
},
"features": [
{
"uri": "https://yappin.leaflet.pub/3mq3ekun3as2v",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Filip β Image used on Comparing minor features of Spotify and YouTube Music"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 8,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@growtika",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 18,
"byteStart": 13
},
"features": [
{
"uri": "https://unsplash.com/photos/diagram-f7uCQxhucw4",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 75,
"byteStart": 27
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mptwf35vlc2j",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Growtika β Image used on Setting up Kubernetes with Cilium and Cloudflare"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 10,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@carrier_lost",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 20,
"byteStart": 15
},
"features": [
{
"uri": "https://unsplash.com/photos/blue-and-red-cargo-ship-on-sea-during-daytime-jOqJbvo1P9g",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 64,
"byteStart": 29
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mptx4b262k2v",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Ian Taylor β Image used on Setting up Argo CD in a k3s cluster"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 12,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@luarte_raw",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 22,
"byteStart": 17
},
"features": [
{
"uri": "https://unsplash.com/photos/assorted-colored-lapel-pin-lot-FHTxbpRWEsE",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 80,
"byteStart": 31
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mpvgufpwwk2m",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Kevin Luarte β Image used on Rehype plugin that turns GitHub links into badges"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 14,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@mahbodakhzami",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 24,
"byteStart": 19
},
"features": [
{
"uri": "https://unsplash.com/photos/brown-pine-cone-on-table-jc9dkaaEYr4",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 99,
"byteStart": 33
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mpvjajgyo22v",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Mahbod Akhzami β Image used on Customize the \"Overview\" title of your Starlight Table of Contents"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 14,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@markuswinkler",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 24,
"byteStart": 19
},
"features": [
{
"uri": "https://unsplash.com/photos/a-wooden-block-spelling-credit-on-a-table-ULXC8_R1sXE",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 83,
"byteStart": 33
},
"features": [
{
"uri": "https://yappin.leaflet.pub/3mq3ehqt2bc2w",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Markus Winkler β Image used on A Stylish Way to Give Credit in Starlight Projects"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 11,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@mboulden",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 21,
"byteStart": 16
},
"features": [
{
"uri": "https://unsplash.com/photos/a-pocket-watch-sitting-on-top-of-a-book-eLqWJqJzJDg",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 91,
"byteStart": 30
},
"features": [
{
"uri": "https://yappin.leaflet.pub/3mq3etm3nec2l",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Meg Boulden β Image used on Designing a No-Account, No-Database Approach to Time Tracking"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 10,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@muriel_1",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 20,
"byteStart": 15
},
"features": [
{
"uri": "https://unsplash.com/photos/a-close-up-of-a-black-surface-with-yellow-balls-CFPWZHBHLRk",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 82,
"byteStart": 29
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mpvd3fc4ss23",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Muriel Liu β Image used on Horizontal progress indicator for your Starlight site"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 10,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@mayurgala",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 20,
"byteStart": 15
},
"features": [
{
"uri": "https://unsplash.com/photos/silhouette-of-persons-hands-forming-heart-2PODhmrvLik",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 55,
"byteStart": 29
},
"features": [
{
"uri": "https://felix.leaflet.pub/3mqaov5uzks2x",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Mayur Gala β Image used on Heartfelt Thanks & Credits"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 20,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@nakkeeran_raveendran",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 30,
"byteStart": 25
},
"features": [
{
"uri": "https://unsplash.com/photos/a-long-line-of-pillars-in-a-building-dxgIrClfpOc",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 90,
"byteStart": 39
},
"features": [
{
"uri": "https://yappin.leaflet.pub/3mq3efefkfs2j",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Nakkeeran Raveendran β Image used on A short history of Starlight Sidebar Topics plugins"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 26,
"byteStart": 0
},
"features": [
{
"uri": "https://neurolaunch.com/editorial-team/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 36,
"byteStart": 31
},
"features": [
{
"uri": "https://neurolaunch.com/wp-content/uploads/2024/09/omega-brain-unlocking-cognitive-potential-with-essential-fatty-acids-1024x585.webp",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 77,
"byteStart": 45
},
"features": [
{
"uri": "https://still-waters.leaflet.pub/3mpv33b5m6k2q",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "NeuroLaunch Editorial Team β Image used on Stop Outsourcing Your Soul to AI"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 18,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@nicolasjleclercq",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 28,
"byteStart": 23
},
"features": [
{
"uri": "https://unsplash.com/photos/round-black-button-pin-on-brown-wooden-surface-iGri85wetNI",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 88,
"byteStart": 37
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mpvhzjwjlk2a",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Nicolas J Leclercq β Image used on SΓ€tteri plugin that turns GitHub links into badges"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 11,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@niko_photos",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 21,
"byteStart": 16
},
"features": [
{
"uri": "https://unsplash.com/photos/green-leaf-tree-under-blue-sky-tGTVxeOr_Rs",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 51,
"byteStart": 30
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mpvwhvlubs2z",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "niko photos β Image used on Uncommon Git Commands"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 14,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@planetvolumes",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 24,
"byteStart": 19
},
"features": [
{
"uri": "https://unsplash.com/photos/a-group-of-smiley-face-balloons-on-a-white-background-3ki14DqTrCQ",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 67,
"byteStart": 33
},
"features": [
{
"uri": "https://felix.leaflet.pub/3mr3uuuabsc2i",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Planet Volumes β Image used on Celebrating World Emoji Day my way"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 13,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@nahakiole",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 23,
"byteStart": 18
},
"features": [
{
"uri": "https://unsplash.com/photos/flat-lay-photography-of-circuit-board-zP7X_B86xOg",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 83,
"byteStart": 32
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mpvcsbo2tk2x",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Robin Glauser β Image used on Starlight Sidebar Topics Dropdown and List together"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 15,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@frostroomhead",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 25,
"byteStart": 20
},
"features": [
{
"uri": "https://unsplash.com/photos/person-on-top-of-brown-steel-frame-xNdPWGJ6UCQ",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 89,
"byteStart": 34
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mptxljhmn22z",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Rodion Kutsaiev β Image used on Setting up Continuous deployment in a GitHub repository"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 12,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@stoke1863",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 22,
"byteStart": 17
},
"features": [
{
"uri": "https://unsplash.com/photos/a-couple-of-cats-laying-next-to-each-other-wajNU4_VxWk",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 63,
"byteStart": 31
},
"features": [
{
"uri": "https://still-waters.leaflet.pub/3mpv3aar7k22n",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Stephen Kidd β Image used on What open source really is about"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 13,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@steve_j",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 23,
"byteStart": 18
},
"features": [
{
"uri": "https://unsplash.com/photos/blue-red-and-yellow-abstract-painting-nDqFutpNNC4",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 84,
"byteStart": 32
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mpvca73jyk2x",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Steve Johnson β Image used on How to make your Starlight sidebar items look better"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 22,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@tingeyinjurylawfirm",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 32,
"byteStart": 27
},
"features": [
{
"uri": "https://unsplash.com/photos/brown-concrete-statue-of-man-9SKhDFnw4c4",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 79,
"byteStart": 41
},
"features": [
{
"uri": "https://still-waters.leaflet.pub/3mpv3fqpkgs2i",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Tingey Injury Law Firm β Image used on Rising community at tomorrow's horizon"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 17,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@towfiqu999999",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 27,
"byteStart": 22
},
"features": [
{
"uri": "https://unsplash.com/photos/a-pile-of-plastic-letters-and-numbers-on-a-pink-and-blue-background-5u6bz2tYhX8",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 89,
"byteStart": 36
},
"features": [
{
"uri": "https://yappin.leaflet.pub/3mq3eybrft22p",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Towfiqu barbhuiya β Image used on Accelerating translations with continuous integration"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 15,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@veronikafitart",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 25,
"byteStart": 20
},
"features": [
{
"uri": "https://unsplash.com/photos/green-plant-on-blue-ceramic-pot-on-brown-wooden-table-bUmtYSRQpHs",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 89,
"byteStart": 34
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mpvkbvkhtc2u",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Veronika FitArt β Image used on Add Additional Content to Starlight's Table of Contents"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 15,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@viktortalashuk",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 25,
"byteStart": 20
},
"features": [
{
"uri": "https://unsplash.com/photos/assorted-files-05HLFQu8bFw",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 65,
"byteStart": 34
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mpvilyrxoc2f",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Viktor Talashuk β Image used on Autogenerated Starlight sidebar"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 9,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@yancymin",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 19,
"byteStart": 14
},
"features": [
{
"uri": "https://unsplash.com/photos/a-close-up-of-a-text-description-on-a-computer-screen-842ofHC6MaI",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 61,
"byteStart": 28
},
"features": [
{
"uri": "https://yappin.leaflet.pub/3mq73uqburs2j",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Yancy Min β Image used on Having fun with my GitHub Profile"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 17,
"byteStart": 0
},
"features": [
{
"uri": "https://unsplash.com/@unclearvision",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 27,
"byteStart": 22
},
"features": [
{
"uri": "https://unsplash.com/photos/a-sound-wave-is-shown-in-the-middle-of-a-pink-background-uBqd-tGQI8o",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 86,
"byteStart": 36
},
"features": [
{
"uri": "https://yappin.leaflet.pub/3mq3en4nz2s2i",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Yassine Ait Tahit β Image used on Earworms and the message hidden within the pattern"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "And last but definitely not least: A sincere thank you to YOU - the readers of my blog posts. Without you, writing would not be nearly as encouraging as it is for me. Much love to everybody!"
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreihocwpxlnfmuadg6knc4asf2alzyhijqt65k5ouqg7tvkyvf43mc4"
},
"size": 127906,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "Overview of all the awesome people who make my blogs possible.",
"publishedAt": "2026-07-09T22:33:18.593Z",
"publication": {
"url": "https://felix.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreifufbuxu7ci3whmdz7wf4lnar4gsacqngucuik3hwdhhjhn6uv7zm"
},
"size": 12676,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Felix",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 39,
"g": 39,
"r": 39,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 624,
"accentText": {
"b": 255,
"g": 255,
"r": 255,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 250,
"g": 252,
"r": 253,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"b": 252,
"g": 254,
"r": 255,
"$type": "pub.leaflet.theme.color#rgb"
},
"accentBackground": {
"b": 30,
"g": 130,
"r": 87,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 30,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 252,
"g": 254,
"r": 255,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Migrating my blog to Leaflet
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mqacr5tr5k2w",
"data": {
"path": "/3mqacr5tr5k2w",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mjjki3asbs2k",
"tags": [
"hello world"
],
"$type": "site.standard.document",
"title": "Migrating my blog to Leaflet",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3234-440a-7882-8521-e6dcfea2e531",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"plaintext": "Hello, World!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Old Apple computer displaying \"hello\" in a colorful and squiggly way.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreideots7jdouufvjs6hmvbksyda4lywceio7tvciyahjcslnwruhtq"
},
"size": 310276,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2400,
"height": 2400
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 104,
"byteStart": 95
},
"features": [
{
"uri": "https://starlight.astro.build/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 134,
"byteStart": 113
},
"features": [
{
"uri": "https://starlight-blog-docs.vercel.app/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 150,
"byteStart": 138
},
"features": [
{
"did": "did:plc:btxrwcaeyodrap5mnjw2fvmz",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
}
],
"plaintext": "This is my new blog home. I recently migrated all my blogs from my previous website built with Starlight and the Starlight Blog plugin to @leaflet.pub. The user experience for those migrations was absolutely fantastic. Just copy the Markdown in the rich text editor, make a few minor adjustments, and tada π"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I have categorized all my previous posts into three major groups:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 170,
"byteStart": 157
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Technical blogs: These are posts that explain concepts, share knowledge and provide code snippets so everyone can benefit from them. I call this publication Desired State. Spoiler: It's mostly about Starlight."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 148,
"byteStart": 136
},
"features": [
{
"uri": "https://still-waters.leaflet.pub/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Mindful posts: Three of my previous write-ups were more thoughtful, in-depth, serious and sometimes philosophical. I grouped them under Still Waters. Let that name sink in..."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 37,
"byteStart": 30
},
"features": [
{
"uri": "https://yappin.leaflet.pub/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Last but not least, I am also yappin' a lot. This is the category which others might call \"Others\". It's for posts that do not fit into the other two categories. But it's the category where I put in the most honest and personal opinions. And sometimes a little rage side quest."
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The reason for the separation is so you can decide which topics interest you the most. Whether you have been a fellow reader or this is your first time reading my work, feel free to check out those publications."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"plaintext": "\"Yeah, but what about this publication?\""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 53,
"byteStart": 36
},
"features": [
{
"uri": "https://felix.leaflet.pub",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "you might ask. Well, let's just say felix.leaflet.pub is like an informal announcements channel. I don't plan to blog very often here, but let's see."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I also still need to figure out the best way to consistently give credit to all the people who make my blog posts possible. So this publication here could soon see a dedicated \"Credits\" post to list them all, that I can update as needed."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Anyways, I hope you are doing well. Have a nice day and enjoy reading!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 13,
"byteStart": 10
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Felix out\nEOF"
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreideots7jdouufvjs6hmvbksyda4lywceio7tvciyahjcslnwruhtq"
},
"size": 310276,
"$type": "blob",
"mimeType": "image/webp"
},
"bskyPostRef": {
"cid": "bafyreicr267p5oaeprz4qf5qlwyfrklu5zyn7wcglydloxexc7oldcn3my",
"uri": "at://did:plc:pbjvqaziagcyv2vqodldn5op/app.bsky.feed.post/3mqacrasfhs2w",
"commit": {
"cid": "bafyreihbqkxdtbjd5quumr5x4pm4c6to7btqnglwrdzwe5z4mm3pdb4gnu",
"rev": "3mqacraxilb2s"
},
"validationStatus": "valid"
},
"description": "Hello, World! and a little update about my blog posts and how I'll structure them in the future.",
"publishedAt": "2026-07-09T18:56:17.698Z",
"publication": {
"url": "https://felix.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreifufbuxu7ci3whmdz7wf4lnar4gsacqngucuik3hwdhhjhn6uv7zm"
},
"size": 12676,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Felix",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 39,
"g": 39,
"r": 39,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 624,
"accentText": {
"b": 255,
"g": 255,
"r": 255,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 250,
"g": 252,
"r": 253,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"b": 252,
"g": 254,
"r": 255,
"$type": "pub.leaflet.theme.color#rgb"
},
"accentBackground": {
"b": 30,
"g": 130,
"r": 87,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 30,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 252,
"g": 254,
"r": 255,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Having fun with my GitHub Profile
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mq73uqburs2j",
"data": {
"path": "/3mq73uqburs2j",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpyxzgvpnk2z",
"tags": [
"story",
"github",
"fun"
],
"$type": "site.standard.document",
"title": "Having fun with my GitHub Profile",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f4351-1d79-7556-9850-5af57ac02cc7",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Visual representation of git history with contributions stats, featuring text highlighted in green and blue for clarity.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreie2c4eloyrsovz7t4biminmxlocd3qm66mkoqdtd4npkjgeeooy3e"
},
"size": 1019080,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2400,
"height": 1590
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I genuinely enjoy playing around with ideas that are completely useless on one hand, but also present very interesting problems to solve on the other hand. So recently I decided to play around with my GitHub user profile and the content it should show."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 221,
"byteStart": 200
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 284,
"byteStart": 283
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f4356-161b-7887-94a3-dd80ffed90a5",
"contentFacets": [
{
"index": {
"byteEnd": 34,
"byteStart": 24
},
"features": [
{
"uri": "https://commonmark.org/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 81,
"byteStart": 57
},
"features": [
{
"uri": "https://github.github.com/gfm/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"contentPlaintext": "Not entirely limited to CommonMark, but still limited to GitHub flavored Markdown"
}
]
}
],
"plaintext": "GitHub has pioneered a really open feature: Allowing users to completely customize their public profiles on their platform only with Markdown by utilizing a naming convention to create special repos (<username>/<username>). As one can imagine, this limits people to Markdown features*. However, developers love solving tricky challenges. And this has led to some really cool and creative inventions, like websites, which generate an SVG that you can embed in your profile Readme."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 50,
"byteStart": 38
},
"features": [
{
"uri": "https://astro.badg.es/contributors/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 59,
"byteStart": 52
},
"features": [
{
"uri": "https://nuxters.nuxt.com/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 81,
"byteStart": 61
},
"features": [
{
"uri": "https://ghstats.dev/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "To name some examples I really enjoy: Astro Badges, Nuxters, GitHub Profile Stats and so many awesome more that this post could go on forever. Many users also handcraft their own creations, and so I decided to join them as well and share my learnings with the community here."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "The idea"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 40,
"byteStart": 29
},
"features": [
{
"uri": "https://medium.com/@jedbrown99/the-bento-grid-principle-2427c95adc40",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Since I was deep diving into Bento grids the other day, my vision of a fitting user profile was a grid of different stats put together in some aligning way."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 270,
"byteStart": 263
},
"features": [
{
"uri": "https://github.com/Platane/snk",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 344,
"byteStart": 343
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f4360-df18-7887-94b8-45706391f457",
"contentPlaintext": "At first, I wanted to make it real-time, but I was too lazy to set up webhooks and I feared that GitHub would ban me if I spammed actions every time the song changes (which happens around every 3 minutes on average when listening to normal music)."
}
]
}
],
"plaintext": "I wanted some static stats, like my name with my profile picture blurred in the background and my Astro contributions. But to make it more challenging, I also wanted some dynamic elements: My GitHub contribution heatmap being eaten by a 2D snake (which is from a project I discovered recently) and a widget of a recent song I played on Spotify*."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "The limitation"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 215,
"byteStart": 186
},
"features": [
{
"uri": "https://github.github.com/gfm/#disallowed-raw-html-extension-",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 223,
"byteStart": 215
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://github.github.com/gfm/#disallowed-raw-html-extension-",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 224,
"byteStart": 223
},
"features": [
{
"uri": "https://github.github.com/gfm/#disallowed-raw-html-extension-",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "When I wrote \"limited to Markdown\", I was lying a little bit. You can also put HTML elements into your Markdown files and they render fine. For security reasons however, GitHub strictly refuses to render stuff like <iframe>s, which to my knowledge is necessary if you want some dynamically embedded stuff (without relying on GitHub actions)."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Another potential solution I played around with were generating images of the dynamically generated content on a schedule in a script called by a GitHub action. But then I would not be able to have continuous animations on the profile."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 177,
"byteStart": 164
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "One info worth mentioning is that GitHub has also built in security measures to not execute fetches from the Markdown. Even when you embed an SVG image, which uses foreignObject, fetches are still blocked. To work around this, we base64-encode the prefetched images."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 91,
"byteStart": 72
},
"features": [
{
"uri": "https://stackoverflow.com/questions/14951321/how-to-display-html-content-in-github-readme-md/65049620#65049620",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 209,
"byteStart": 200
},
"features": [
{
"uri": "https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "There were some other ideas along my little journey, but a very helpful StackOverflow entry demonstrated a workaround to get more HTML possibilities out of GFM. This hint together with base64-encoded data URLs allowed me to open Pandora's box π₯³"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "The implementation"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The final setup works like this:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.orderedList",
"children": [
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Every 30 minutes, a GitHub action triggers, which executes a build script."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 66,
"byteStart": 59
},
"features": [
{
"uri": "https://spotify-github-profile.kittinanx.com/api/view?uid=pjyifjjapvah0yoy2zvd8vx0v&cover_image=true&theme=default&show_offline=false&background_color=0d1117&interchange=true&bar_color=53b14f&bar_color_cover=true",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 83,
"byteStart": 71
},
"features": [
{
"uri": "https://github.com/Platane/snk/tree/main/svg-only",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 84,
"byteStart": 83
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f4573-4800-7cc4-b44b-5fd720a947f1",
"contentPlaintext": "Strictly speaking, there is no Snake API call.Β Arthur Brongniart provided a fully functional GitHub action, which generates the animated SVG Snake visualization of your GitHub contribution graph. This is saved in the dist folder, so it can be read from inside the script."
}
]
}
],
"plaintext": "This script fetches two APIs for the two dynamic elements: Spotify and snk/svg-only*"
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "It then base64-encodes the resulting SVG files and creates data URLs."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Those huge URLs can then be included in a bento grid HTML template with prefilled styles and desired layout."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 54,
"byteStart": 41
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "This generated HTML is then wrapped in a foreignObject tag in an SVG, which is committed to the repository."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The Readme then just has to include the SVG with Markdown image syntax and since all data is stored within the SVG (no external fetches anymore), it renders on my profile."
}
}
],
"startIndex": 1
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 112,
"byteStart": 103
},
"features": [
{
"uri": "https://github.com/trueberryless",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "It's not beautiful internally, but its presence shines. Feel free to check out the final result out on my GitHub."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreie2c4eloyrsovz7t4biminmxlocd3qm66mkoqdtd4npkjgeeooy3e"
},
"size": 1019080,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "A little story about limitations of READMEs on GitHub and how to circumvent them.",
"publishedAt": "2025-02-05T08:19:00.000Z",
"publication": {
"url": "https://yappin.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreihbmln2jwabdtivgz4vz3snhf52mh4om7h5gqpnv4z6ueiostwm3q"
},
"size": 14608,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Yappin'",
"$type": "site.standard.publication",
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 43,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 250,
"g": 252,
"r": 253,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Just yappin' words 'cause therapy doesn't come with SEO.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Vibe-coding a VS Code Extension for Terraform projects
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mq3fijas2s25",
"data": {
"path": "/3mq3fijas2s25",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpyxzgvpnk2z",
"tags": [
"terraform",
"automation",
"plugins"
],
"$type": "site.standard.document",
"title": "Vibe-coding a VS Code Extension for Terraform projects",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3e24-23e7-7990-be13-55eb183db978",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "A wooden church building perched on a hill, symbolizing stability and faith, akin to resolving variables in Terraform.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreidmvxez7sn4nanolbh7eloxnrsyvibpnb6n4652f64ojq4ftmhp4i"
},
"size": 936202,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2400,
"height": 1925
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Today, I want to talk about a small VS Code extension that I vibe-coded in a single day to make working with Terraform easier. I will first explain what Terraform is, and then jump right into the functionality and reasons behind this extension. Feel free to skip the first section if you are already familiar with Terraform."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "What is Terraform?"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 173,
"byteStart": 164
},
"features": [
{
"uri": "https://github.com/hashicorp",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 291,
"byteStart": 278
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "Terraform is an infrastructure-as-code (IaC) tool that lets you define your entire cloud or on-prem infrastructure as code. Its configuration language, invented by HashiCorp, is called HCL (HashiCorp Configuration Language) and features an intuitive, human-readable syntax. The native syntax has the advantage that the learning curve is low, it is consistent for all providers and pleasant to read and write for humans, whilst the JSON-based variant is easier for machines to generate and parse."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 68,
"byteStart": 59
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 80,
"byteStart": 73
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "What makes Terraform very powerful are two basic features: variables and modules. Variables enable you to create resources where the actual values can be inserted from somewhere else and modules group any number of resources, which has the advantage of reusability and nesting. While both features sound very basic, the combination of the two makes a dream team. Because the openness of Terraform enables you to define input variables for each module and also export variables via outputs."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "A simple module is therefore structured like this:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "azure-resource-group/\nβββ main.tf\nβββ variables.tf\nβββ outputs.tf\nβββ README.md",
"syntaxHighlightingTheme": "catppuccin-latte"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 24,
"byteStart": 17
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 52,
"byteStart": 43
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "All files except main.tf are optional (the README.md isnβt Terraform-specific), but I prefer following best practices, so the example includes them."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 119,
"byteStart": 107
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 275,
"byteStart": 264
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 287,
"byteStart": 283
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Let's look at a simple example. Imagine we want to manage a resource group in Azure with Terraform. In the variables.tf file we define what inputs we expect inside the module. Each of the two blocks represents the variable and properties of the variable, like the description or the type:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "hcl",
"plaintext": "// variables.tf\nvariable \"name\" {\n description = \"Name of the resource group\"\n type = string\n}\n\nvariable \"location\" {\n description = \"Azure region\"\n type = string\n default = \"westeurope\"\n}",
"syntaxHighlightingTheme": "catppuccin-latte"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 40,
"byteStart": 32
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 68,
"byteStart": 61
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 216,
"byteStart": 209
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "You might have noticed that the location variable also has a default property, which makes the variable optional and provides a fallback value if none is specified. These variables can then be used inside the main.tf file. In here, we define the actual resources we want to generate:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "hcl",
"plaintext": "// main.tf\nprovider \"azurerm\" {\n features {}\n}\n\nresource \"azurerm_resource_group\" \"this\" {\n name = var.name\n location = var.location\n}",
"syntaxHighlightingTheme": "catppuccin-latte"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 190,
"byteStart": 180
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "In this case, we just define our simple resource group, which you can think of as a folder in Azure - very amateurish explanation. Now the last part which we can implement, is the outputs.tf file:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "hcl",
"plaintext": "// outputs.tf\noutput \"resource_group_id\" {\n description = \"The ID of the resource group\"\n value = azurerm_resource_group.this.id\n}",
"syntaxHighlightingTheme": "catppuccin-latte"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "This is very useful if we want to use information like the ID of the resource group later in other parts of our Terraform project."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 33,
"byteStart": 24
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "As I already wrote, the README.md is not actually part of the Terraform definitions, but helps us understand the purpose of the module:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "markdown",
"plaintext": "# Azure Resource Group Module\n\nA minimal Terraform module that creates an Azure Resource Group.\n\n## Usage\n\n```hcl\nmodule \"rg\" {\n source = \"./azure-resource-group\"\n name = \"my-resource-group\"\n}\n```",
"syntaxHighlightingTheme": "catppuccin-latte"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 16,
"byteStart": 7
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 245,
"byteStart": 238
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 259,
"byteStart": 247
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 274,
"byteStart": 264
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 332,
"byteStart": 324
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 373,
"byteStart": 361
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "As the README.md already suggests, we can now use this module to create an Azure Resource Group anywhere inside our project. We just need to make sure that the path to the module folder is correct and Terraform knows how to interpret the main.tf, variables.tf and outputs.tf files. In this case, since we do not specify the location variable, it will be set to \"westeurope\"."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "hcl",
"plaintext": "module \"rg\" {\n source = \"./azure-resource-group\"\n name = \"my-resource-group\"\n}",
"syntaxHighlightingTheme": "catppuccin-latte"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I hope this section gave you a better basic understanding of what Terraform is and what you can do with it."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "The tedious task of Terraform"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 38,
"byteStart": 32
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "You might have noticed that the source field of a module creates an awesome flexibility for the whole project since you can define modules anywhere and use them anywhere inside the project."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "However, this feature also has a huge drawback when it comes to maintainability: Resolving the actual values behind variables can be very tedious. While in this simple example it might seem easy to manage, the bigger the project grows, the more nested it is, the harder and more time-consuming understanding and adapting Terraform code can be. Therefore, I made an extension."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 29,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 366,
"byteStart": 359
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "What should the extension do? The main goal of the extension is to provide the actual values of the variables everywhere you use them. If a module gets called from multiple places or just multiple times, the information should provide which use case sets the variable to which value. If modules get nested deeply, the values should resolve until we reach the .tfvars files. These are the files where you can define the hard-coded values for variables."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "How to start a VS Code extension"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 113,
"byteStart": 109
},
"features": [
{
"uri": "https://github.com/nodejs",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Creating the basic template project for a VS Code extension is as easy as running this command when you have Node installed:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "npx --package yo --package generator-code -- yo code",
"syntaxHighlightingTheme": "catppuccin-latte"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "This command temporarily installs the Yeoman (a generator software), and enters an interactive mode where some questions have to be answered, like the project name, type and description. In the following block you can see all questions and default answers:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "# ? What type of extension do you want to create? New Extension (TypeScript)\n# ? What's the name of your extension? HelloWorld\n### Press <Enter> to choose default for all options below ###\n\n# ? What's the identifier of your extension? helloworld\n# ? What's the description of your extension? LEAVE BLANK\n# ? Initialize a git repository? Y\n# ? Which bundler to use? unbundled\n# ? Which package manager to use? npm\n\n# ? Do you want to open the new folder with Visual Studio Code? Open with `code`",
"syntaxHighlightingTheme": "catppuccin-latte"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 97,
"byteStart": 71
},
"features": [
{
"uri": "https://code.visualstudio.com/api/get-started/your-first-extension",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "For further information regarding VS Code extensions I want to link to the official documentation, as it is always up-to-date in contrast to my explanations here."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "3β¦ 2β¦ 1β¦ Vibe-Coding!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 46,
"byteStart": 39
},
"features": [
{
"uri": "https://github.com/openai",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "I like to start new projects by asking ChatGPT for general technical questions like:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "How would you suggest achieving such a project?"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "How long do you estimate it will take me?"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "How difficult will it be?"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Brainstorming with AI bots seems like a very sensible way to get a better overview of new projects, especially when diving into completely new waters."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Creating a VS Code extension was exactly such a use case because I had never created anything for VS Code or any other IDE before."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 62,
"byteStart": 56
},
"features": [
{
"uri": "https://github.com/anthropics",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "After this initial brainstorming phase, I prefer to use Claude for programming all the files necessary for such an extension since in my experience this AI bot really has the ability to nail code. I hardly encountered bugs or runtime issues with Claude code, so I use it exactly for such use cases β initial starters. The drawback of Claude is its very limited free trial, which is why I am always careful when choosing my words and try to think of as many possible mistakes the AI could make ahead of time."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 199,
"byteStart": 187
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "So this is how it began: I took some time to formulate my exact scenario and wish into the limited context windows of a random words machine called Claude and immediately received a huge extension.ts implementation. Of course there were obvious differences between my vision and the communication to this LLM, so I had to tell it another time what the important considerations are and in order to be able to receive more code, I also used my magic trick: \"If this is too big for one file, just give me the whole extension.ts in multiple files, which should work of course.\""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Claude then went on a long journey."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "After some struggling to keep it going my tokens were all used and I had to figure out the remaining code myself. Nonetheless, I managed to get a running version since not much was missing and immediately realised some improvements that went unnoticed until now. Since I had to wait for my tokens to refill, I did exactly this."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The next morning the improvement session began, and after two long conversations, the context window was completely full and I could not even send messages in the same chat any more, although my tokens were not expired yet. However, all I wanted to achieve and get out of Claude was now available and I could run some tests in one of my bigger projects. After confirming and adjusting some code manually, I was satisfied for now and so I decided that the first public release was ready."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 40,
"byteStart": 26
},
"features": [
{
"uri": "https://github.com/github",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 81,
"byteStart": 61
},
"features": [
{
"uri": "https://code.visualstudio.com/api/working-with-extensions/publishing-extension#create-a-publisher",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Some trial and error with GitHub Actions later, I managed to set up the publisher for a VS Code extension. And two patch versions later, I was quite happy with the results."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "The Final Result"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 184,
"byteStart": 178
},
"features": [
{
"uri": "https://github.com/trueberryless/terraform-variables-resolution",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Try out the extension yourself by clicking on the button below. There are still many improvements to be made, so if you are interested in helping out, feel free to contribute on GitHub."
},
"alignment": "lex:pub.leaflet.pages.linearDocument#textAlignLeft"
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"url": "https://marketplace.visualstudio.com/items?itemName=trueberryless.terraform-variables-resolution",
"text": "Install Terraform Variables Resolution",
"$type": "pub.leaflet.blocks.button"
},
"alignment": "lex:pub.leaflet.pages.linearDocument#textAlignLeft"
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 59,
"byteStart": 34
},
"features": [
{
"uri": "#what-is-terraform",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Here you can see the example from the Terraform explanation, showing how this extension visually adds the values of the variables in VS Code:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Terraform Variables Resolution Example",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreidz43pflu6s6247zvpivwjssimi7d7ehdakm52jr3rh24iambq3xa"
},
"size": 8152,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 418,
"height": 105
}
},
"alignment": "lex:pub.leaflet.pages.linearDocument#textAlignLeft"
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": ""
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreidmvxez7sn4nanolbh7eloxnrsyvibpnb6n4652f64ojq4ftmhp4i"
},
"size": 936202,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "How I created an extension for Visual Studio Code which adds inline resolutions to variables in Terraform projects.",
"publishedAt": "2025-08-01T20:01:00.000Z",
"publication": {
"url": "https://yappin.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreihbmln2jwabdtivgz4vz3snhf52mh4om7h5gqpnv4z6ueiostwm3q"
},
"size": 14608,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Yappin'",
"$type": "site.standard.publication",
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 43,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 250,
"g": 252,
"r": 253,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Just yappin' words 'cause therapy doesn't come with SEO.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Accelerating translations with continuous integration
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mq3eybrft22p",
"data": {
"path": "/3mq3eybrft22p",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpyxzgvpnk2z",
"tags": [
"automation",
"github",
"plugins",
"markdown",
"tool"
],
"$type": "site.standard.document",
"title": "Accelerating translations with continuous integration",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3e22-7efd-7559-af45-14b31154c3c7",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Vibrant plastic letters in various colors set against a pink and blue background, adding a playful touch to the image.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreigsrtijptcqqstqykikahqmmlshrezrkfmqxkfymu4hysilrp5waa"
},
"size": 273114,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2400,
"height": 1601
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "For the last year, I have been working a lot in various Open Source Communities on GitHub in my free time and I have been enjoying these somehow relaxing contributions because they help me gain new knowledge on a daily basis."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 236,
"byteStart": 231
},
"features": [
{
"uri": "https://github.com/withastro/astro",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "After some time contributing I also got to know how kind and welcoming communities behind those projects are. These people have all one thing in common with you: They want to build great stuff in their leisure time. Especially the Astro community is the one and only I have enjoyed being in the most, since it's the most rewarding and friendly at the same time. Not all communities can achieve such a great status among OSS."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 80,
"byteStart": 59
},
"features": [
{
"did": "did:plc:npyn64qisvc4epm6iqnjza24",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
},
{
"index": {
"byteEnd": 286,
"byteStart": 277
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Recently, I discovered another evolving project founded by @pelikhan.bsky.social which aims to automatically translate all your documentation, blogs or shop sites whenever something changes with the help of AI. Whilst initially sceptical, reading more about the project in its README.md had me hooked."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The approach of strictly guiding AI into only translating little chunks - mostly sentences or paragraphs with perfectionist instructions - made me think that this could really work. So I tried it out in my own project, my blog. And to my surprise, those translations were much better than I expected. Of course you notice that it was translated by AI - handmade work will hopefully never be replaced entirely - but if you fairly mention that you use AI in an aside, this is much more accessibly for people than nothing."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "So I took a closer look at the actual code of the project. After noticing some little inconsistencies, spaces for improvements and missing but very cool features here and there, I started contributing there as well. I mean, after one year of contributions in bigger project, PRs were nothing new to me."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 88,
"byteStart": 84
},
"features": [
{
"uri": "https://github.com/pelikhan",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "You have to know that this was a Friday evening - almost night in my timezone - and Peli was currently online, also doing commits after commits to the project. I quickly noticed that, in contrast to the other projects I am part of, Peli did not waste any time merging PRs. It felt very rewarding this night because I just kept going until 1am, Peli merging faster than I could improve features, docs and fixes. Another aspect that felt great was that since this was a young project, Peli did not hesitate to just merge, try it out locally and then pushing little fixes I missed."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 192,
"byteStart": 183
},
"features": [
{
"uri": "https://starlight.astro.build/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Fast-forward three days and I am now an official contributor to the project, making it greater and greater almost daily, mainly focusing on docs and the support for my heart project: Starlight."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 229,
"byteStart": 223
},
"features": [
{
"uri": "https://github.com/pelikhan/action-continuous-translation",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Peli and I have our virtual productivity sessions between 4pm and 8pm UTC, extending longer on weekends. So if you want to join us as an early bird of the project or got interested in using it, feel free to check it out on GitHub."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreigsrtijptcqqstqykikahqmmlshrezrkfmqxkfymu4hysilrp5waa"
},
"size": 273114,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "The story of how my journey of contributions to action-continuous-translation started.",
"publishedAt": "2025-07-09T19:52:00.000Z",
"publication": {
"url": "https://yappin.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreihbmln2jwabdtivgz4vz3snhf52mh4om7h5gqpnv4z6ueiostwm3q"
},
"size": 14608,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Yappin'",
"$type": "site.standard.publication",
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 43,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 250,
"g": 252,
"r": 253,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Just yappin' words 'cause therapy doesn't come with SEO.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Designing a No-Account, No-Database Approach to Time Tracking
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mq3etm3nec2l",
"data": {
"path": "/3mq3etm3nec2l",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpyxzgvpnk2z",
"tags": [
"annoucement",
"tool"
],
"$type": "site.standard.document",
"title": "Designing a No-Account, No-Database Approach to Time Tracking",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3e20-4b55-7776-8091-84d0d2077260",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "An elegant pocket watch placed on a book, highlighting the contrast between the timepiece and the textured pages.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreigfv5zjj4s4bmeohtnirfluswdj4skmhw3vxhe5cu2okwxt3pg43i"
},
"size": 581722,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2400,
"height": 1590
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 363,
"byteStart": 357
},
"features": [
{
"uri": "https://vercel.com/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "The summer of 2024 marked the start of an exciting new chapter for me as a developer. Up until that point, I'd worked primarily with Angular, a framework I appreciated for its structured and opinionated approach. But I'd always been curious about React β it felt lighter, more flexible, and justβ¦ different. When I came across Next.js, a framework from Vercel built around React that offered features like server-side rendering and a focus on performance, I knew it was time to dive into something new."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 263,
"byteStart": 231
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "At the same time, I'd been frustrated for ages with session tracking tools like Toggl. While they're great in theory, their best features always seemed locked behind premium paywalls. Like every developer at some point, I thought, βWhy not just build my own?β It's practically a rite of passage β every programmer has to create a todo app or a tracker at least once in their career. And so, True Tracker was born."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "True Tracker wouldn't just be any tracker, though. I wanted to do something different. For starters, I wanted to keep it simple: users could add projects, break them down into tasks, and track their time in sessions. No bloat, no unnecessary features. But here's the twist β True Tracker would save everything in local storage. No accounts, no cloud sync, and most importantly, no one (not even me) would have access to your data. It's your tracker, and your data stays yours."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 187,
"byteStart": 164
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
},
{
"index": {
"byteEnd": 504,
"byteStart": 496
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Of course, transitioning from Angular to Next.js wasn't without its challenges. If there's one piece of advice I'd give anyone learning a new framework, it's this: Read the documentation. Seriously, don't skip it. I didn't, and it led to chaos. Next.js had recently transitioned to the app router, but I stubbornly started building with the page router. Turns out, this caused a mountain of problems when the app wouldn't build properly because there was no app directory. I had to keep an empty .gitkeep file in the folder just so Git wouldn't delete it and break everything again. Not my proudest moment."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 211,
"byteStart": 194
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "After two weeks of nonstop coding, debugging, and running into what felt like every error imaginable, True Tracker finally worked. I remember that first successful run β it felt like shouting βIt's alive!β in my head."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 190,
"byteStart": 176
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "The project also wasn't without its jokes. A friend, after seeing the app's branding, quipped, βSoβ¦ the 'true' in True Tracker? That's a bit much, even for the image. It's too much true!β They weren't wrong, but I embraced it. After all, if you're going to brand something, you might as well commit."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 177,
"byteStart": 139
},
"features": [
{
"uri": "https://true-tracker.trueberryless.org",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "True Tracker became my first-ever Next.js project β a simple but unique app for tracking time with no strings attached. It's live now at https://true-tracker.trueberryless.org."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The journey taught me more than just how to use Next.js. It taught me about pushing past challenges, transitioning between frameworks, and embracing the quirks that make each project unique. True Tracker isn't just an app; it's a milestone β a little piece of proof that even small ideas can have a big impact."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreigfv5zjj4s4bmeohtnirfluswdj4skmhw3vxhe5cu2okwxt3pg43i"
},
"size": 581722,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "A private time-tracking app storing all data in your browser β no accounts, no database, just simple tracking.",
"publishedAt": "2024-07-06T19:49:00.000Z",
"publication": {
"url": "https://yappin.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreihbmln2jwabdtivgz4vz3snhf52mh4om7h5gqpnv4z6ueiostwm3q"
},
"size": 14608,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Yappin'",
"$type": "site.standard.publication",
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 43,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 250,
"g": 252,
"r": 253,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Just yappin' words 'cause therapy doesn't come with SEO.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Some aspects of creating a blog are really easy, and some are extremely difficult!
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mq3eqn2c4k2c",
"data": {
"path": "/3mq3eqn2c4k2c",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpyxzgvpnk2z",
"tags": [
"education",
"mindspace",
"blogging"
],
"$type": "site.standard.document",
"title": "Some aspects of creating a blog are really easy, and some are extremely difficult!",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3e1e-ab14-722c-9727-335856d825ba",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "A wooden table displays a notebook, a pen, and a steaming coffee cup, suggesting a productive writing session.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreifd2u4zzul44w7y7li5t6brg4nj7eipxkumo6umddyu2rr4qsnv54"
},
"size": 909716,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2400,
"height": 1600
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 48,
"byteStart": 43
},
"features": [
{
"uri": "https://astro.build",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 56,
"byteStart": 49
},
"features": [
{
"uri": "https://astro.build/chat",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 108,
"byteStart": 104
},
"features": [
{
"uri": "https://github.com/alexanderniebuhr",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "After a very interesting discussion in the Astro Discord β started by none other than core maintainer Alex himself (btw congratulations on this recent achievement π) β, I joked around that someone could write a blog about reasons why it is so difficult for many people to not only write, but also maintain a blog as a whole."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 60,
"byteStart": 55
},
"features": [
{
"uri": "https://github.com/jdtjenkins",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 280,
"byteStart": 218
},
"features": [
{
"uri": "https://jacobjenkins.codes/posts/why-is-writing-blogs-so-hard/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "The man who nominated me as an Astro Support Squad β Jacob (btw thank you very much for noticing me within this awesome community π) β took that quite literally and wrote a blog about it. No joke. Check it out: https://jacobjenkins.codes/posts/why-is-writing-blogs-so-hard/ This is his very first blog btw (and hopefully not his last one, as I really enjoyed reading his style)."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 72,
"byteStart": 69
},
"features": [
{
"uri": "https://github.com/louisescher/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 293,
"byteStart": 275
},
"features": [
{
"uri": "https://lou.gg/blog/why-writing-blogs-isnt-hard",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Our beloved everywhere-helper, Blogger, Designer and StudioCMSer β Lou (btw thank you for giving me some blog writing tips in advance so I can more confidently write this π€©) β jumped immediately on this awesome hype-train (as Twitch users might call it) and published his follow-up post within fricking 12 hours!!! Believe me, I checked the commits, Lou literally pushed within 4min of the 12h mark."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "As I will include many references to both of their blogs, it's best if you read theirs first. And then mine. Or maybe it's better if you just read theirs, I'm not yet sure how mine will develop."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 18,
"byteStart": 15
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#strikethrough"
}
]
}
],
"plaintext": "But what I can say write, is that mine will fully focus on just the writing skills."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "How to setup a blog (technical challenges)"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 232,
"byteStart": 226
},
"features": [
{
"uri": "https://medium.com",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 241,
"byteStart": 234
},
"features": [
{
"uri": "https://leaflet.pub",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 247,
"byteStart": 243
},
"features": [
{
"uri": "https://pckt.blog",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 257,
"byteStart": 249
},
"features": [
{
"uri": "https://offprint.app",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 412,
"byteStart": 398
},
"features": [
{
"uri": "https://starlight-blog-docs.vercel.app/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 432,
"byteStart": 426
},
"features": [
{
"uri": "https://hideoo.dev",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 574,
"byteStart": 567
},
"features": [
{
"uri": "https://www.netlify.com/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Whilst this blog doesn't focus on how to build a blog, deploy it and all this technical stuff, I want to mention that everyone can do it. Like Lou mentioned, as a non-technical person, you can always just create an account on Medium, Leaflet, Pckt, Offprint and other alternatives. If you are a little into programming, you can let your creativity a little bit more space and thanks to the awesome Starlight Blog project from HiDeoo you do not need much knowledge to deploy your own website. Nowadays, where there are also PaaS (Platform as a Service) providers like Netlify, which make deploying stuff a Click-Ops adventure, there no more is any excuse in not being able to publish your blog because of technical difficulties."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Okay, jokes aside, now: Writing skills"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 273,
"byteStart": 244
},
"features": [
{
"uri": "https://docs.astro.build/en/install-and-setup/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 380,
"byteStart": 352
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 414,
"byteStart": 406
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 502,
"byteStart": 498
},
"features": [
{
"uri": "https://docs.astro.build/en/reference/errors/invalid-content-entry-data-error/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "But this blog should focus on mainly on the \"Writing Skill\" as I said, so let's continue there. As I myself initially had a hard time writing blogs, I can totally agree with Jacob that getting started is hard. It's not like reading through the Astro \"Getting started\" guide where you just have to follow the instructions and then ask on Discord what a InvalidContentEntryDataError means β please use the #support thread if you do π«Ά β (If you are still wondering what this error means, click here)."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "On the other hand, the goal seems so close and doable. Just write some words. What's so hard in writing those fricking words? Actually, I myself don't know. Why are you asking me? The only thing that helped me getting out of the spiral of \"You-Want-To-Write-But-Every-Time-You-Stare-At-The-Blinking-Cursor-Your-Brain-Goes-Numb\" (let's make that the hashtag of the week) was Lou's tip of not wanting as much of yourself in your first blog. Why do you want a perfect blog to start with? Just because of your reputation? So people don't think wrong of you? Feel disappointed? To be honest: That's bullshit! Not just because your first blog will very likely not be read by anyone (with the exception of Jacob's exceptional blog), but also because you are just making it harder for yourself. My personal mind ceiling was that I thought I must write at least 500 words to begin with. But who says that? To be honest, if I could look back, I sometimes think: \"Man, it would have been so unique and cool if my first ever blog was just like a regular tweet\", or even extremer: One. Word. Nobody will stop you from publishing this blog:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"plaintext": "Hello π"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "(Sorry if I now took your awesome idea,\nand included your whole blog post in here"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "β wow cool, that rhymes)"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 246,
"byteStart": 235
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 257,
"byteStart": 253
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
},
{
"index": {
"byteEnd": 357,
"byteStart": 352
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
},
{
"index": {
"byteEnd": 362,
"byteStart": 359
},
"features": [
{
"uri": "https://lou.gg/blog/why-writing-blogs-isnt-hard#:~:text=Back%20to%20blog",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 605,
"byteStart": 601
},
"features": [
{
"uri": "https://jacobjenkins.codes/posts/why-is-writing-blogs-so-hard/#:~:text=hrad",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "See? I can do whatever I want, even being a fricking poet (typical German behaviour), and nobody will judge me. Okay, maybe someone will hate on this blog because it's \"a waste of time, no meaning in all this useless content\", but who _c a r e s_? It's your blog, nobody asked for their opinions (unless you have a comment section under your blog π cough (Lou). So don't think about what others might think about your writing style, content, use of language or even if you make grammar mistakes. Those all make you human and getting everything perfect is not only completely unnecessary, it's also hrad."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Another part of writing blogs is not just getting started, but the difficulty of maintaining one flow, one thought if you will. As a listener of well-written speeches you subconsciously notice that it was good, it was fulfilling, sublime. And that's what I love about such blogs or speeches. They just feel right somehow. But I myself have accepted that I can't accomplish such achievements in my own creative work. And I'm much better off with that acceptance. Because the benefit of accepting imperfection is getting things done. Not just 50%, not just 90%. Done. Completed, 100%. And this is something that many people struggle with, my past self included β actually just myself included."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "But as Lou mentions, there are other easy methods to keep a common thread in the whole post, sticking to the story whilst thoughts might wander around in your own brain. Bringing all those thoughts on paper, or in this case on the keyboard can be challenging, especially when you are starting writing. But there is a reason why we learn such skills at school, they improve our cognitive health and development immensely. This is also why I heavily agree with Lou when he states that AI should never be used in the process of writing. It just has no benefits, neither for the reader and even less for you."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "So I don't want to repeat what Lou says, but bullet points have helped me out a lot in the past. They just give me the feeling of constant structure, while my mind can play around with different thoughts and theories. This makes sure that you always come back to what you initially wanted to include in your post. \"And if you can't come up with bullet points?\", you might ask. Then you might need some more brainstormings or just showers."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "In the end, these are some tips I can give you along the way:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Develop your own writing style, not by training, but just writing. Sooner or later you will notice what types you might like, some things you want to adopt from other bloggers, but summa summarum what you write will always be yours. You just share it with the world. It's up to you if you want to use a colon or hyphen here and there, use more dots, the past tense, imperativ, swear words or the more formal style. All those things will come automatically together and define you as a unique blogger. And you readers will value this individuality more than you know."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Don't take on too much. Might sound easy, but everyone falls regularly into this trap. Or maybe just people like me, having a mix of autism and perfectionism β idk."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Although it always sounds stupid: Start writing. And if it doesn't work, start writing the middle part. Because I know start and ending are always difficult, but imo the middle part goes down like butter."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Blogs have easy and hard sides: This is what the title says, but know that when I wrote all that I realised that I haven't answered that question at all... Thank god, that Jacob didn't type a question mark at the end of that suggestion. π"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 96,
"byteStart": 74
},
"features": [
{
"uri": "https://lou.gg/blog/why-writing-blogs-isnt-hard",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Yeah, I guess that's it. If you want to share you thoughts, leave them in Lou's comments section."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreifd2u4zzul44w7y7li5t6brg4nj7eipxkumo6umddyu2rr4qsnv54"
},
"size": 909716,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "A beginner friendly introduction with some swear words on how to start writing your own blog post.",
"publishedAt": "2025-07-08T19:48:00.000Z",
"publication": {
"url": "https://yappin.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreihbmln2jwabdtivgz4vz3snhf52mh4om7h5gqpnv4z6ueiostwm3q"
},
"size": 14608,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Yappin'",
"$type": "site.standard.publication",
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 43,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 250,
"g": 252,
"r": 253,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Just yappin' words 'cause therapy doesn't come with SEO.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Earworms and the message hidden within the pattern
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mq3en4nz2s2i",
"data": {
"path": "/3mq3en4nz2s2i",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpyxzgvpnk2z",
"tags": [
"mindspace"
],
"$type": "site.standard.document",
"title": "Earworms and the message hidden within the pattern",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3e1d-68ff-7ddc-ad01-1698e48ff5c4",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "A visual representation of a sound wave set on a pink background, showcasing the dynamics of sound frequencies.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreid3x626c3vvdhpc3hht6kthd2z6eg7lxvabtz2i2a4ir3gf2bho3e"
},
"size": 134894,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2400,
"height": 1417
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Yes, this title is a small nod to the excellent Netflix series \"Arcane\", but that's not what this post is about."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 240,
"byteStart": 228
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "Today, I want to share some thoughts about earworms, a term that originated in German (βOhrwurmβ) and was later adopted into English as a literal translation. The meaning behind this rather abstract term is catchy tunes β sticky music, as Wikipedia calls it. This phenomenon often occurs unpredictably, especially when our thoughts drift away from the present moment."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 211,
"byteStart": 196
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "While many of my blog-post ideas strike on train rides, while lying in bed, or when I'm simply reflecting on life, the inspiration for this post literally hit me whilst showering, as the good old shower thoughts are intended to be. The reason β as you might be able to guess from the topic β was an earworm about a melody where I could have sworn that I haven't heard it for a long time, like 4β5 weeks."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 480,
"byteStart": 461
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "And this always seems to be the confusing parts about earworms: Nobody really knows who decides when to play. Is it the brain hitting βplayβ when itβs a little numb? You know what I mean, those moments when you don't really know what you are currently thinking and you don't have to think about what to think, you are just enjoying the moment. There is another Austrian idiom for this exact scenario: \"ins Narrenkastl schauen\", which means something like to stare into space. And I think that exactly those absent-minded moments are the origin and reason why earworms happen."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 320,
"byteStart": 304
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "But this still leaves the question: Who decides what to play in someone's head when it happens? Like who is the DJ? From my own experience I have noticed a repeating pattern, especially if you have just been listening to music in the last half of an hour. My brain at least, likes to repeat parts of the penultimate song heard, in other words the song, which was played before the last song. This theory works even better if the last song was paused in the middle or just not perfectly at the end of it."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The exact reason for that is completely unknown to me, but if I had to guess, I would say that the rhythms of the completed song (the penultimate one) are still ringing somewhere in your mind since the song itself was maybe one of those \"I-just-fade-out-my-volume-to-end-me\"-songs, which is not my favourite type of ending to music, but it encourages the brain to go on."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "At least that's what I think. I would be very interested if you have experienced similar patterns, so feel free to write your own blog post about it or just share this one with friends so you can discuss it with them!"
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreid3x626c3vvdhpc3hht6kthd2z6eg7lxvabtz2i2a4ir3gf2bho3e"
},
"size": 134894,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "Some random thoughts about earworms and their message hidden within the pattern.",
"publishedAt": "2025-07-23T19:46:00.000Z",
"publication": {
"url": "https://yappin.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreihbmln2jwabdtivgz4vz3snhf52mh4om7h5gqpnv4z6ueiostwm3q"
},
"size": 14608,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Yappin'",
"$type": "site.standard.publication",
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 43,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 250,
"g": 252,
"r": 253,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Just yappin' words 'cause therapy doesn't come with SEO.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Comparing minor features of Spotify and YouTube Music
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mq3ekun3as2v",
"data": {
"path": "/3mq3ekun3as2v",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpyxzgvpnk2z",
"tags": [
"review"
],
"$type": "site.standard.document",
"title": "Comparing minor features of Spotify and YouTube Music",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3e1b-c579-7663-91ec-1071f450ed1d",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Streaming platform displayed on a phone with headphones beside it, illustrating music access options.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreidscz2q4kd2fszc3pukis7yqmrbvie3xewpnhhgrgcigh56t5lejy"
},
"size": 197832,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2400,
"height": 1600
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Since Spotify recently updated their prices across all available premium plans, I decided to test out one competitive platform: YouTube Music from Google."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "While many reviews and comparisons focus on the major differences, like price, algorithms, music recommendations, playlist creations and other promoted features by those platforms (including garbage AI features), this post focuses on unnecessary comparisons between little things most people don't care about."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Crossfade"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Spotify has made me love a simple but yet so powerful feature called \"Crossfade\". It allows users to personalize how many seconds (0-12s) between the current and the following song should be overlapped to reduce \"non-playing\" time because many songs lower the volume in the end, so you don't strictly benefit listening to the potential silence."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "YouTube Music doesn't offer such functionalities and there are also many other features I wish to see in more streaming platforms that Spotify has built upon the years:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Listening to music together with Jams"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Generating new playlists from single tracks"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Add one song to multiple playlists at the same time"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Shuffle"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 224,
"byteStart": 216
},
"features": [
{
"uri": "https://newsroom.spotify.com/2025-11-13/shuffle-update-fewer-repeats/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "While for hard-core reddit users and maybe even intermediate music enthusiasts it is probably common knowledge that the Spotify shuffle algorithm is completely broken and unfair towards lesser known artists, Spotify recently added a new \"Fewer Repeats\" shuffle mode, which ironically is enabled by default as if Spotify is admitting that its old algorithm is less recommendable. Whether the option between the new and the old, now so-called \"Standard\" shuffle adds more confusion or finally satisfies all users that have been begging for such a seemingly simple fix for multiple years by now, will yet have to be decided by each individual listener."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "While it might seem for most YouTube Music listeners that the shuffle algorithm of their streaming platform \"just works\", in my opinion the UX is far from perfect. The option to shuffle a playlist or album with one click is nowhere near to be seen because you either need to always listen to the first song of your playlist before being able to activate shuffle or you need to fumble around to find the hidden menu item \"Shuffle play\" with winter gloves. Definitely more convenient in Spotify where shuffle stats are saved between plays, respecting your preference much better."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Volume"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 331,
"byteStart": 326
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 339,
"byteStart": 333
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 348,
"byteStart": 344
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 384,
"byteStart": 380
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "This is clearly the only section where YouTube Music leads the way in my opinion, while sacrificing your hearing ability at the same time. Although Spotify allows to configure the maximum volume deeply nested in the settings somewhere (Settings β Playback β Volume Controls β Volume Level) with three different options: Quiet, Normal and Loud, I am really dissatisfied that Loud on 100% is equivalent to about 80% of the non-customizable YouTube Music volume. And while I get that too loud listening activities are just simply bad for your health β no debate about that β, we all know that everyone once was in their 20s where we don't care about our hearing capabilities in later life."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Adding songs to playlists"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I know there are a lot of people out there that have never created their own playlist as streaming services already offer great managed playlists with your music style ready to be saved to your library. Nonetheless, I am this kind of guy who wants to believe that they have control over their own playlist and when I discover a new song, I want to add it to all of my playlists where this song fits into. Spotify made some huge but in my opinion very beneficial redesigns of exactly this process. Therefore, they now offer a great overview where you can ideally manage the songs in your playlists."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "In YouTube Music, songs have to added one-by-one to your playlists which is a tedious process if you have around 20 playlists that are based on moods and one song usually fits into multiple moods or lets say categories."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Mixes and Smart Shuffle"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Both music services offer quite good options for enhancing your static playlists with similar songs if you feel like discovering new songs that would fit besides known and loved ones. Spotify once again gives you much more options however, as there is the \"Smart Shuffle\" mode which temporarily adds those songs to your playlist until disabled. Moreover, they have the \"Go to song radio\", which generates a whole playlist based on the song you currently vibe most to. This playlist can then optionally be saved to your library of course. On YouTube Music, the radio feature is called \"Start mix\" which just puts similar songs into the queue, not allowing you to save the awesome mix as a playlist."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "To confuse you even more, Spotify also offers a \"Mix\" feature on playlists that allows complete customization of the transition between songs with equalizers, fade-ins and βouts, bpm controll and some more features I do not understand as a casual listener. Although this might be very cool for transition lovers and DJs starters, I personally can not see any potential benefit for normal music lovers and would much more prefer to be able to loop over specific parts of songs instead, which would be super helpful if the hook of one song is extremly good, but you don't like the boring rap part in between."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Track Sorting"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Yes, the order of my tracks in my playlists matter. Spotify and YouTube Music offer great ways to sort your playlist, with Spotify once again offering more flexibility by allowing sorting by title, artist, album and date added. YouTube Music really thinks that dates are everything I care about β well maybe I do."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Playlist Sorting"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Not so important for most people I would assume, but still relevant enough to be written about in this post of untalked features: Spotify once again allows to order your playlists, albums and artists in your library by recent listening activity, recent song additions, title and also creator. Plus they offer pins so you can always expect to hit your favourite playlist when you know the positioning by heart. This seems like a little advantage but trust me, the speed difference is incredible."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 218,
"byteStart": 213
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "Talking about speed, YouTube Music offers the \"Speed dial\" which are basically pins but on your Home tab (?) It takes some time to get used to, especially since other playlists are already listed on the home page above your speed dial, which one could argue makes them more quickly accessible with your thumb. Nonetheless, sorting playlists in my library should be a must-feature. And sorry if I am just too stupid, but I still don't understand what the difference between \"Recent activity\" and \"Recently played\" is supposed to be, especially when you consider the third option: \"Recently saved\". YouTube Music should really think about their DAU (Dumbest Assumable User) or offer info boxes. Either way, removing one of those three options and adding alphabetical order would be my recommendation for improvements."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Library Filtering"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Not much to write, I really enjoy the UX and UI design of both YouTube Music and Spotify. Both offer wunderful tabs for playlists, artists, albums and more that each offer \"sub-filtering\" as I like to call it, e.g. \"Subscriptions\" or \"By you\", \"By Spotify\" and \"Downloaded\", respectively for YouTube Music and Spotify. As you can once again see, Spotify simply offers more and they feel smarter and also faster. Not sure why the YouTube Music tabs have a little .5s delay, but it ruins the flow of picking a little bit."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Lyrics"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "In general, it feels like YouTube Music offers lyrics on more songs than Spotify, but I didn't run any fancy benchmarks. Both services are largely accurate compared to what I believe is sung. Since I have been using Spotify for much longer now, I can only remember mismatches on the former platform. However, AFAIK lyrics are provided by Musixmatch and Spotify offers somewhat okay-ish ways to report wrong lyrics. Personally, I would love to be able to also suggest the correct lyrics when submitting feedback, as how should Musixmatch know which part of the lyrics are the reasons for my reporting action."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "YouTube Music probably has the advantage in this point since they use both Musixmatch and LyricFind and as I know Google probably some additional AI comparison to fill the gaps."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Discoverability"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "YouTube Music people mostly rave about the awesomeness of the YouTube algorithm and its understanding of your liking which leads to better discoverability of new songs. Admittedly, the algorithm of Google even after a short time of usage feels much more suited than what Spotify would have been able to learn from my habits for over three years now, which is incredible on the one hand but totally understanable and comprehensible on the other hand, given that Google almost exclusively makes billions of dollars every year thanks to nearly perfect advertisment match-making. What I try to say is: Google has the advantage because their business model relies on a secret, yet powerful algorithm, while Spotify's primary selling point is simply Spotify. Correct me if I'm wrong in the comments, pls!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "As a result, YouTube Music has the better algorithm and understands very well what you want to listen to at any point in time. Their recommendations are delightful and therefore, the discoverability of new songs feels like sensory overload. Spotify's recommendations are decent, once in a while I really listen to new songs that I actually enjoy and save into at least five playlists at once thanks to the overview feature."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Integrations and Ecosystem"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 261,
"byteStart": 260
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f3e1b-f609-7002-88f3-722a33a18638",
"contentPlaintext": "The one thing I miss when controlling music from another device on Spotify is volume control. When my headphones are connected to my phone and I work on my laptop, I want to be able to control the volume nonetheless. I understand that this is another topic when joining a jam, where only the host should be able to control the volume to avoid trolls, but within my own devices, this seems like a reasonable feature."
}
]
},
{
"index": {
"byteEnd": 500,
"byteStart": 492
},
"features": [
{
"uri": "http://stats.fm",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 712,
"byteStart": 704
},
"features": [
{
"uri": "http://stats.fm",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "And now the final argument, why β as you might have noticed β I prefer Spotify over YouTube Music: The fr*cking ecosystem of Spotify. While many talk about the obvious advantage of syncing what you currently play between all your devices and having (almost*) full control of what you play on another device, which is pretty cool, I am writing about the little benefits here: The Raycast Spotify integration makes controlling music while working on my MacBook extremely fun and enjoyable. stats.fm just produces awesome daily, weekly, monthly and overall stats about my listening activity on Spotify: I know exactly, how many times I have listened to each song and if I continue to list advantages of stats.fm the post would be twice as long. Nonetheless, I have to mention the weekly creation of amazing looking images with my top artists and songs of the week."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "YouTube Music doesn't offer any cool stats, the Raycast integration needs the YouTube Music \"app\" (it's a wrapper of the webpage) downloaded which doesn't even work when using nix-darwin. Public API endpoints would really benefit YouTube Music to allow third-party apps to visualize your music habits."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "And even more important, the integrations that exists (obviously with YouTube itself) are terrible. Google doesn't split my music and the videos I watch in any reasonable way. I mean: Why on earth would I want to see all my library playlists on my YouTube profile and have to scroll all the way down to finally be able to find the \"Watch Later\" video playlist ?!? If I wanna watch videos, I wanna go into the YouTube app and if I wanna listen to music, I wanna go into the YouTube Music app. It's that simple. No need for some automatically generated YouTube Music playlist \"Liked music\", which shows YouTube videos that are by pure coincidence also music videos at the same time. I understand that this separation is really hard, as the two platforms evolved as one and are now supposed to serve two different purposes. But Google charges the apps separately, so the should also provide this separation in the apps. This is the point where I think about adding a \"Rage\" tag to the post."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 11,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "To be fair: YouTube Music offers a simple toggle under Settings β General β \"Show your liked music from YouTube\" which allows users to filter your \"Liked music\" playlist for only tracks you liked on YouTube Music. However, the other way around for whole playlists would also be very beneficial. I simply don't want to see my music playlists on my YouTube profile, private or public, whatever."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Stats for nerds"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "My ratings (stars or whatever you want to imagine):"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Spotify: 9/10"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "YouTube Music: 6.5/10"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Distribution of points from this post (by heading):"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Spotify: 8/11 (Crossfade, Shuffle, Adding songs, Mixes, 2x Sorting, Filtering, Ecosystem)"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "YouTube Music: 3/11 (Volume, Lyrics, Discoverability)"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Conclusion"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Spotify offers way more customization, flexibility and settings."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "YouTube Music just knows what you want to hear."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "All in all, I find it pretty sad that the music industry is so broken, where artists need 1000x more listeners to achieve the same profit compared to hard copies like they were available before I was born. And since the whole industry is basically controlled by the 'Big Three' β Universal Music Group (UMG), Sony Music Entertainment (SME), and Warner Music Group (WMG) β it is impossible for Spotify to be profitable in the long run, with competitions like YouTube Music which do not necessarily need to raise prices as the companies behind all other platforms have other apps that make enough money for compensation."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "To be honest, all this comparison and deep write is a waste of time, let's just go back to gramophone records. Nonetheless, share your disagreements and maybe also your positive opinions in the comments below. Happy listening and lower the volume from time to time!"
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreidscz2q4kd2fszc3pukis7yqmrbvie3xewpnhhgrgcigh56t5lejy"
},
"size": 197832,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "This post compares some small quality-of-live features between two of the most popular music streaming platforms.",
"publishedAt": "2025-11-25T20:45:00.000Z",
"publication": {
"url": "https://yappin.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreihbmln2jwabdtivgz4vz3snhf52mh4om7h5gqpnv4z6ueiostwm3q"
},
"size": 14608,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Yappin'",
"$type": "site.standard.publication",
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 43,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 250,
"g": 252,
"r": 253,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Just yappin' words 'cause therapy doesn't come with SEO.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}A Stylish Way to Give Credit in Starlight Projects
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mq3ehqt2bc2w",
"data": {
"path": "/3mq3ehqt2bc2w",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpyxzgvpnk2z",
"tags": [
"starlight"
],
"$type": "site.standard.document",
"title": "A Stylish Way to Give Credit in Starlight Projects",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3e1a-5822-7aa7-8676-f4e50675e664",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Visual representation of a credit score, highlighting key metrics and overall credit health.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreibafm5fl4jtrwgumjttmb22fczwqa5yl2w5k3exivfs7mgc4jbknq"
},
"size": 1218490,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2400,
"height": 1600
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 58,
"byteStart": 49
},
"features": [
{
"uri": "https://starlight.astro.build",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 76,
"byteStart": 66
},
"features": [
{
"uri": "https://docs.astro.build",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "It all started when I noticed the credit link to Starlight in the Astro docs. Positioned at the bottom of the Table of Contents, it was simple but stylish, and I loved how it subtly gave credit. When Starlight introduced its own credit option, I activated it in all my projects. It was small, understated, and tucked at the bottom of each page β nice, but easy to miss."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 102,
"byteStart": 25
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "Thatβs when I thought, What if others want a cooler credit option, like the one from the Astro docs? So, I set out to create a plugin. Initially, it was just a fun project to link to Starlight and Astro in a more noticeable way. I started with two presets β one for Starlight and one for Astro β and shared my work on the Astro Discord."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 60,
"byteStart": 32
},
"features": [
{
"uri": "https://www.rainsberger.ca/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 113,
"byteStart": 72
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
},
{
"index": {
"byteEnd": 151,
"byteStart": 129
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "Thatβs where things took off. One awesome Astro maintainer suggested, \"Why not link to the Starlight Blog too?\" and I thought, What a brilliant idea! I added the blog as a third preset, and suddenly, the plugin was more than I had ever planned."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "But I wasnβt done yet. It bugged me that the credits were always in English, so I decided to make them translate automatically based on the languages supported by Starlight. Thatβs when the real adventure began. I reached out to the community for help translating the plugin into different languages. The response was incredible! People from around the world offered to pitch in, and what started as a solo project turned into a global collaboration. Along the way, I made new friends and learned so much from everyone involved. Now, the plugin supports around ten languages, and more translations keep coming."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 98,
"byteStart": 75
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "What started as a simple idea turned into something Iβm really proud of: Starlight Cooler Credit . Itβs more than just a plugin β itβs a way to give credit with style, customization, and a touch of global teamwork."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 95,
"byteStart": 45
},
"features": [
{
"uri": "https://starlight-cooler-credit.trueberryless.org/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "If youβre curious, you can check it out at https://starlight-cooler-credit.trueberryless.org/. Give it a try β I think youβll love it as much as I do!"
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreibafm5fl4jtrwgumjttmb22fczwqa5yl2w5k3exivfs7mgc4jbknq"
},
"size": 1218490,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "Inspired by the Starlight credit in Astro docs, I made a customizable plugin that links to Starlight, Astro, and the Starlight Blog, with multilingual support.",
"publishedAt": "2024-11-12T20:43:00.000Z",
"publication": {
"url": "https://yappin.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreihbmln2jwabdtivgz4vz3snhf52mh4om7h5gqpnv4z6ueiostwm3q"
},
"size": 14608,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Yappin'",
"$type": "site.standard.publication",
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 43,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 250,
"g": 252,
"r": 253,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Just yappin' words 'cause therapy doesn't come with SEO.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}A short history of Starlight Sidebar Topics plugins
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mq3efefkfs2j",
"data": {
"path": "/3mq3efefkfs2j",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpyxzgvpnk2z",
"tags": [
"starlight",
"story"
],
"$type": "site.standard.document",
"title": "A short history of Starlight Sidebar Topics plugins",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3e18-9931-7cc1-bd90-29e9ffcea53f",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Majestic pillars stand in the courtyard of an ancient temple, showcasing intricate carvings and historical significance.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreifoodtqw7ohbdy4fhcr4xrnp5mm5dzdy7py3yg7xr6yurrm5ngibq"
},
"size": 1008488,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2400,
"height": 1417
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 189,
"byteStart": 180
},
"features": [
{
"uri": "https://starlight.astro.build",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 339,
"byteStart": 296
},
"features": [
{
"uri": "https://www.websitecarbon.com/website/starlight-astro-build-getting-started/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Maybe you have heard about this cool documentation framework before. I talk about it quite often and use it regularly. Not only because I'm an active contributor, but also because Starlight just grew to my heart. Its features: Simplicity and minimalism, but everything you need. Its performance: Faster than 98% of other websites out there. Its accessibility: Not even a question!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "The Creation"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 210,
"byteStart": 199
},
"features": [
{
"did": "did:plc:n2bc7yctwi2h4twqywulfl5s",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
},
{
"index": {
"byteEnd": 492,
"byteStart": 468
},
"features": [
{
"uri": "https://github.com/hideoo/starlight-sidebar-topics",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 645,
"byteStart": 628
},
"features": [
{
"uri": "https://starlight-sidebar-topics.trueberryless.org/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "However, one thing that's objectively missing in my opinion is a granular way of separating broad topics inside your documentation. And I'm not alone with this subjectively general accepted opinion. @hideoo.dev is not only one of the most active maintainers of the project, but also the author of the most and imho best plugins you can find for Starlight. And they have also noticed this missing niche functionality about topics. That's why they decided to create the Starlight Sidebar Topics plugin early on, back in October 2024, which allows you to make a sidebar with topics. Read more about the plugin's functionalities in its documentation."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 344,
"byteStart": 319
},
"features": [
{
"uri": "https://github.com/HiDeoo/starlight-sidebar-topics/issues/2#issuecomment-2410196392",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 448,
"byteStart": 443
},
"features": [
{
"uri": "https://astro.build",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 483,
"byteStart": 466
},
"features": [
{
"uri": "https://github.com/delucis",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 516,
"byteStart": 500
},
"features": [
{
"uri": "https://github.com/withastro/docs/pull/9890",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "The one thing that bothered me personally in the early days of the plugin was the way topics were displayed in the sidebar. Because it doesn't β like I imagined a solution could look like β use some kind of dropdown menu for switching between topics, but instead always shows all topics. This design choice was, as HiDeoo clearly points out, very intentional and not without any reason: All topics should be visible at once. Proven by the Astro docs themselves (Chris Swithinbank implemented the \"tabbed sidebar\" for the Astro v5 docs later in the same month), this approach definitely has many benefits over a dropdown menu. Nonetheless, I was still unsatisfied by this design, and so I created my own version."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 229,
"byteStart": 217
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 639,
"byteStart": 606
},
"features": [
{
"uri": "https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Copying and pasting was my strength when it came to building a new Starlight plugin for the community. And so, I did just that. I took the Starlight Sidebar Topics plugin as a starting point and just had to adapt the Topics.astro component, which includes the HTML for rendering the topics in the sidebar. After struggling for some time with the implementation of a satisfying enough dropdown menu, which was pure HTML + CSS but also clean to look at (sorry for the self-praise, I'm just proud of myself), I finally found a solution that I'm very satisfied with and released this new plugin under the name Starlight Sidebar Topics Dropdown β how original."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "The Adaptation"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 248,
"byteStart": 232
},
"features": [
{
"uri": "https://deep-thoughts.pckt.blog/from-balloons-to-binary-a-20th-birthday-packed-with-clues-wga364p",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 339,
"byteStart": 334
},
"features": [
{
"uri": "https://github.com/withastro/starlight/releases/tag/@astrojs/starlight@0.32.0",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 644,
"byteStart": 581
},
"features": [
{
"uri": "https://bsky.app/profile/hideoo.dev/post/3liffpudc5c2b",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "You might think that this story is now over because everyone could just use the variant they want, everyone is happy. But Starlight kept and keeps improving and around three months later, on the 15th of February, coming almost as a birthday present for me, was the long-awaited addition of route data in Starlight with the release of v0.32. This seemingly small addition opened up so many possibilities, which I myself never saw coming. HiDeoo once again was the leading inspiration for plugin authors and pushed this new functionality to its limits. It took them just two days to adapt over 11 plugins to the latest Starlight version back then and they later improved and refactored much of the code to make even more use of this powerful feature."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "And so comes the day when HiDeoo contacts me to tell me that the Starlight Sidebar Topics plugin now uses Starlight's new route data feature. At first, I didn't quite understand what benefits these implementation changes would have, but then HiDeoo took the time to explain to me that I could now turn the Starlight Sidebar Topics Dropdown plugin into a simple component which uses the route data from his plugin. Intuitively, I was opposed to this smart idea because it felt like my only plugin that made some popularity was turned into some useless component. I couldn't have been further from the truth."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "The Union"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 160,
"byteStart": 110
},
"features": [
{
"uri": "https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/pull/40",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 232,
"byteStart": 220
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 352,
"byteStart": 346
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#strikethrough"
}
]
},
{
"index": {
"byteEnd": 505,
"byteStart": 494
},
"features": [
{
"uri": "https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown/releases/tag/starlight-sidebar-topics-dropdown%400.5.0",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Eventually, I decided to refactor my plugin into a component β this refactoring casually eliminated exactly 1210 lines of code and added 68 lines of changelog β and I noticed, how little code it now took to turn the Topics.astro list into a dropdown from a user's perspective. I was confident that this really was the right direction for the plugin component. And so I updated the whole documentation β more like: deleted more than 200 lines of text (feels good) β and released the new version 0.5."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 259,
"byteStart": 254
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "Now you might wonder how such mature plugins could still improve over time. To be honest, I myself was very surprised when HiDeoo casually dropped a bomb in my Discord DMs. His idea was and still is ingenious. Otherwise, I wouldn't even think about this topic anymore. But here I am, writing a whopping 800 words just to prepare you for what's to come. Drum roll please! Fasten your seatbelt! This statement from HiDeoo is going to blow your mind:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"plaintext": "\"One could have something like the default built-in list on a desktop view, and on mobile use your component, or something like that π§ \" β HiDeoo, 21/03/2025"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Profound. Timeless. Golden."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 149,
"byteStart": 92
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mpvcsbo2tk2x",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "And that single, beautiful spark of an idea? Itβs precisely what Iβll guide you through in the post \"Starlight Topics Dropdown and List together\"."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreifoodtqw7ohbdy4fhcr4xrnp5mm5dzdy7py3yg7xr6yurrm5ngibq"
},
"size": 1008488,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "In this blog post, I'll show you the evolution of Starlight plugins with a case study of the Starlight Sidebar Topics plugin.",
"publishedAt": "2025-05-05T19:41:00.000Z",
"publication": {
"url": "https://yappin.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreihbmln2jwabdtivgz4vz3snhf52mh4om7h5gqpnv4z6ueiostwm3q"
},
"size": 14608,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Yappin'",
"$type": "site.standard.publication",
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 43,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 250,
"g": 252,
"r": 253,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Just yappin' words 'cause therapy doesn't come with SEO.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}From Balloons to Binary: A 20th Birthday Packed with Clues
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mq3cbav26s2f",
"data": {
"path": "/3mq3cbav26s2f",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpyxzgvpnk2z",
"tags": [
"story",
"birthday"
],
"$type": "site.standard.document",
"title": "From Balloons to Binary: A 20th Birthday Packed with Clues",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3de6-c233-7aa7-809d-865f774b5fc5",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "The final image shows me sitting in the middle of the living room surrounded by birthday items, most of which are part of the puzzle.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreiettdmqcaotvpr3yrej2riner3jriad7j2ewff5ohjeqtyetwjn3u"
},
"size": 2269472,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 4624,
"height": 3472
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "I Turned 20 - And Turned It Into a Puzzle"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "This year, I turned 20 β and instead of simply lighting candles and inflating balloons, I wanted to celebrate in a more creative and personal way. The idea? Hide the number 20 in as many surprising, clever, and hidden forms as possible throughout a decorated living room β then capture it all in a single photo. Whether through mathematical puzzles or visual Easter eggs, the room transformed into a joyful riddle full of playful detail."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The final result wasn't just a festive setting, but a carefully designed puzzle layered with meaning, humor, and subtle nods to things I love. Some clues are easy to spot, others are deeply hidden or require some creative interpretation β but now that the challenge has been out in the wild, it's time for the official reveal."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 64,
"byteStart": 56
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "Altogether, the number 20 (or variations of it) appears 73 times throughout the image. That includes 14 instances of the literal number β20β hidden in clever forms like barcodes, QR codes, and hex values, as well as six times where objects appear in exact quantities of twenty. There are also 32 appearances of β20β printed on birthday balloons scattered across the room β although about half of them are not visible from the camera's perspective - and five additional references that creatively reflect my birthday through numbers like 2025 or 2005."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Roughly 40 of these clues are clearly visible and solvable from the photo alone. The rest require some lateral thinking or imagination to uncover β especially those hiding just beyond the 2D frame of the image."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "How the β20β Was Hidden"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Many of the 20s are woven directly into the decor. There's a bright 20 km/h road sign, giant rainbow foil balloons forming the digits 2 and 0, and a huge βHappy 20th Birthdayβ banner stretched across the window. A birthday βcakeβ made out of 20 toilet paper rolls sits prominently near the center, surrounded by decorative items like clocks, fruit, Ferrero Rocher chocolates, party candles, and game pieces β all grouped intentionally into quantities of 20."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Then come the mathematical twists: two QR codes subtly lead to the number 20. There's a Morse code strip, a binary code (10100), and a color represented in hexadecimal that converts to 20 in base 10. A barcode hides the number visually, while a hand of playing cards (Ace, Jack, Queen, King) cleverly adds up to 20. A domino pair, too, sums to the magic number. Even a slice of cake on the couch β precisely 20% of the whole β continues the theme."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 335,
"byteStart": 334
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f3dea-f3de-7002-88c3-597019a3bb51",
"contentFacets": [
{
"index": {
"byteEnd": 292,
"byteStart": 275
},
"features": [
{
"uri": "https://en.wikipedia.org/wiki/Dice#Arrangement",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"contentPlaintext": "Fun fact: The tops of the dice should add up to 20, but in the final picture, they only add up to 12. That's because the five dice weren't manufactured with the same construction arrangement β two follow a clockwise numbering and three a counterclockwise one, as explained here on Wikipedia. π"
}
]
}
],
"plaintext": "Some of the trickiest clues are tucked into logic puzzles. For example, the apples and pears on the wooden wall shelf resemble 1s and 0s, and if decoded correctly, they spell out βFeβ β a subtle nod to my name, Felix. Dice on the table are turned so that you can deduce the hidden top sides, which, when summed up, result in 20.* And the most challenging: a bundle of Mikado sticks, whose total visible point value is 110 β which, when interpreted in base 4, equals exactly 20 in decimal. A true puzzle within the puzzle!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "A few clues also fall under the βpop culture knowledgeβ category. There's a mention of Rihanna, who, like me, was born on February 20. A small tribute to Hans Zimmer and Christopher Nolan nods to their 2005 collaboration on Batman Begins β the same year I was born."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "A Snapshot of My Musical Taste"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "While not directly part of the 20-themed puzzle, there's a musical layer hidden in the image that reflects something deeply personal: my love for music. In the top-left corner, eight square album covers are arranged neatly, each featuring a Spotify Code linking to one of my all-time favorite tracks. Slightly offset to the right, the ninth cover represents a playlist that brings all these songs together β a small curated soundtrack of my world."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Each song on that list has earned its place not just for how it sounds, but for the stories, memories, and emotions it carries. And most importantly, for the incredible artists who created them."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 23,
"byteStart": 15
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "It starts with Molossus from Batman Begins, a powerful, rising track that introduced me to the epic blend of film and music through the work of Hans Zimmer and Christopher Nolan β one of my favorite director-composer duos. The film was released in 2005, the year I was born, and while The Dark Knight is my personal favorite, Molossus will always feel iconic."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 24,
"byteStart": 8
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "Next is 9-bit Expedition from the Fastfall soundtrack, composed by Lifeformed. Their calm, chiptune-inspired style hits a sweet spot that I keep coming back to. I admire their work so much that I've used their tracks in custom Celeste levels I've built β not just because they fit, but because their music means something to me."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 30,
"byteStart": 17
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "And then there's Resurrections from Celeste β a game I've easily spent 200-300 hours playing. The real magic behind its atmosphere is Lena Raine, whose music has a way of reaching into your emotions and pulling out something raw and beautiful. Her compositions are more than just background β they are the experience. I genuinely consider her one of the most inspiring composers I've ever listened to, and her work on Celeste is a masterpiece I'll never stop admiring."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 48,
"byteStart": 19
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "Also in the mix is 22:12 - Until the End of Time by Danger, from the Haven soundtrack. I haven't even played the game β the music alone pulled me in. It's cinematic, emotional, and completely immersive. Danger absolutely nailed it, crafting a world through sound that I could get lost in for hours."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 10,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "Annihilate from Across the Spider-Verse is another standout. I love both movies in the series, and this track perfectly captures the energy, style, and emotion of Miles' story. It's a sonic explosion that never gets old."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 47,
"byteStart": 37
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "From a totally different genre comes ROTTWEiLER by TJ_beastboy, a German rapper who, to me, is on a completely different level. He blends chill vibes and upbeat intensity in a way that feels fresh and real. His music always has something unique to say, and I honestly think he's one of the best artists out there right now."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 41,
"byteStart": 38
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "Another track I couldn't leave out is 747 from Tenet β another mind-bending Nolan film, scored by Ludwig GΓΆransson. The energy, pacing, and structure of this piece hit so hard. It's easily one of my favorite movie soundtracks of all time, and GΓΆransson continues to prove he's a genius when it comes to cinematic music."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 29,
"byteStart": 25
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "Last but far from least: MORE by K/DA β my most-played song ever. It's high-energy, polished, confident, and just straight-up fun. K-pop in general is way too underrated, and this track is proof that genre boundaries don't matter when a song just works on every level."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "These songs β and the brilliant minds behind them β aren't just background music. They're part of my creative world, my emotional landscape, and the soundtrack to everything I love doing."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"gap": 8,
"$type": "pub.leaflet.blocks.imageGallery",
"format": "grid",
"images": [
{
"alt": "Golden 8 playlist cover - a collection of my favorite songs.",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreigssoqmpde6vxs4zdkgb55wdi4oyd7yj3sydsejooi7v5ps4uwiri"
},
"size": 548226,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2480,
"height": 3100
}
},
{
"alt": "Resurrections from Celeste game soundtrack by Lena Raine.",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreibjfbhvvpgif7llfssuwesqoob3xrqjwjdpbui2wwwlzjks7mxk6m"
},
"size": 738434,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2480,
"height": 3099
}
},
{
"alt": "MORE by K/DA - K-pop track.",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreichydtfowpb77wwjbvdxtwhu7om4h4byec5nzh3dyfjbxihmz2l5m"
},
"size": 636126,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2480,
"height": 3099
}
},
{
"alt": "9-bit Expedition from Fastfall soundtrack by Lifeformed.",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreihzw6fdtlipss25v2wesuxr3lrq6jhgex676nwizapu4jtfleg35q"
},
"size": 117512,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2480,
"height": 3099
}
},
{
"alt": "Molossus from Batman Begins soundtrack by Hans Zimmer.",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreid6myfe6ec24r4ymvclbronu65s2jsorss6donj6kpuh4rcy4tcua"
},
"size": 626366,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2480,
"height": 3099
}
},
{
"alt": "747 from Tenet soundtrack by Ludwig GΓΆransson.",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreib6fwjsctiyknaguarxmmtbzwf2fzzifya7bw4h6ut5xsbtkcn43e"
},
"size": 536902,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2480,
"height": 3099
}
},
{
"alt": "22:12 - Until the End of Time by Danger (Haven soundtrack).",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreies4lw2jtkdp74jentikggdenf6g55czvxt7hsyybhmix7wbp5bai"
},
"size": 584868,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2480,
"height": 3099
}
},
{
"alt": "Annihilate from Across the Spider-Verse soundtrack.",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreiake7rn4ahoowb3pduvonp6en3vhqbl7vgjwcvmyc3zcwjkrybq6y"
},
"size": 454104,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2480,
"height": 3099
}
},
{
"alt": "ROTTWEiLER by TJ_beastboy (German rap).",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreicoj2aat2c6va2yecftkquub7sbsvmk4fbw3hlt5rk4d7o2zuchb4"
},
"size": 1148592,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2480,
"height": 3099
}
}
],
"maxWidth": 200
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Stats for Nerds"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "For those who enjoy data and structure, here's a little summary of all 20s in the image:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "73x | Total hidden instances"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "~40x | Clearly visible and solvable "
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "32x | Birthday balloons printed with \"20\" "
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "**14x | Written \"20\" "
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "6x | Groups of 20 identical items "
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "5x | Creative variations *(e.g., 2025, 2005)*"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Puzzle Solutions"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "QR Codes | 20.02.2005"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Bar Code | 20. February"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Binary Code | Today is February 20th, 2025"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Base 64 Code | My birthday is February 20th, 2005"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Hex Code (Color Codes) | twenty"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Morse Code | 20. FEBRUARY 2005"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Playing Cards | King (4) + Queen (3) + Jack (2) + Ace (11) = 20"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Domino | (0|2) + (3|3) + (4|0) + (2|1) + (1|0) + (2|2) = 20"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Apples (0) and Pears (1) | Binary Code: \"Fe\""
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Mikado Sticks Value | 110 (Base 4) = 20 (Base 10)"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Dice top sides | 3 + 2 + 1 + 1 + 5 = 12"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.imageGallery",
"images": [
{
"alt": "Barcode representing '20. February'.",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreicm5mdupmayvkqbf3frlbntve2dfvo5vkud5ugcpfcwj46x255ohi"
},
"size": 42702,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 3509,
"height": 2480
}
},
{
"alt": "Base64 code encoding 'My birthday is February 20th, 2005'.",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreieskzod5v52hka35owyvev6qhajos5mtokinfagcxk5zljcrpwgw4"
},
"size": 108702,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 3509,
"height": 2480
}
},
{
"alt": "Binary code representing 'Today is February 20th, 2025'.",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreianklvbg4uv26uqqysw3632bkaegsz5fwswrc5yvt42n4eznc35ka"
},
"size": 202628,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 3509,
"height": 2480
}
},
{
"alt": "Hex color code representing 'twenty'.",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreibsrpcx6lbpzjz64sr2ficbw65syze4omgxug6ld6npdtmmixag2y"
},
"size": 44834,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 3509,
"height": 2480
}
},
{
"alt": "Morse code for '20. FEBRUARY 2005'.",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreifzrcjjnw6az6tcq5haxw3ly72ipoehtbya52dvmeh6t5hrtdavs4"
},
"size": 31828,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 3509,
"height": 2480
}
},
{
"alt": "QR code encoding '20.02.2005'.",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreih6n4f5kopdaunkydllunl5o4fi5ibgsw4nd2473nn7t2ysp344wa"
},
"size": 172962,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 3509,
"height": 2480
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Conclusion"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "It was a joy to turn this celebration into a personal challenge and to see friends and family try to solve the riddle. Whether you spotted five 20s or all seventy-three β thank you for playing along! π"
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreigey7tmeyzjpysg23buneolaqhrcmpnak3emlzci4juy2zhlavnoq"
},
"size": 948098,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "This is the story of creating a true art masterpiece.",
"publishedAt": "2025-02-20T19:05:00.000Z",
"publication": {
"url": "https://yappin.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreihbmln2jwabdtivgz4vz3snhf52mh4om7h5gqpnv4z6ueiostwm3q"
},
"size": 14608,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Yappin'",
"$type": "site.standard.publication",
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 43,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 250,
"g": 252,
"r": 253,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Just yappin' words 'cause therapy doesn't come with SEO.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}The tale of trueberryless
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mq3bamhb4k2m",
"data": {
"path": "/3mq3bamhb4k2m",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpyxzgvpnk2z",
"tags": [
"creativity",
"username",
"tale"
],
"$type": "site.standard.document",
"title": "The tale of trueberryless",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3db7-7a7f-7119-b815-b929688949e1",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "A digital display is framed by vibrant trees, creating a peaceful natural environment.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreie233kmtghrqcnrozqqbtlteu4lajv5nmsz2wszhvowk2xu7vabgu"
},
"size": 324218,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2504,
"height": 1492
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 412,
"byteStart": 411
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f3ddd-3c6c-7002-88a8-11583c0c015c",
"contentPlaintext": "Yes, that's what I like to call them, given how our brains have devolved with their presence."
}
]
}
],
"plaintext": "On a sunny day in the summer of 2022, I decided to take the first step of a journey I still do not know the end of. I decided that in order to make my dreams of creating a unique identity come true, I would need a recognizable identifier. Back then, identifier meant username to me, so I started looking for a short, abstract username that was guaranteed to be available on all the major asocial media platforms*."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 532,
"byteStart": 531
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f3dce-c055-7002-8890-6fe7fc2b0867",
"contentPlaintext": "As my personality changed over the years, the latter requirement has been neglected now and then, which diverged from the original idea of anonymity and often resulted in a casual reveal of my personal identity behind the username. Nonetheless, I am still very proud of my unique creation."
}
]
}
],
"plaintext": "If you have a more or less common first and last name like mine - Felix Schneider - finding a starting point for something like this is harder than one might think. On the one hand, I wanted to create an identifier abstract enough so I would love it throughout different phases of life. This means it can't include information about something I enjoyed only at the time. On the other hand, it should ideally have some connection that bound it to me without revealing my identity to prevent my username from growing distant from me.*"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 257,
"byteStart": 252
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 366,
"byteStart": 365
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f3dd4-efaa-7002-889b-1a9a139eaf84",
"contentPlaintext": "Note that this was, of course, not my first random word. I had to try several combinations of prefixes and suffixes together with the \"connection word\" until I was happy with it."
}
]
}
],
"plaintext": "Since I couldn't think about anywhere to start, I decided to pick one random English word that I have a personal connection with, while the term is so broad that tracing it back to the origin is almost impossible, while also being of no use to others: berry. I'll let you guess in the comments what connection this term has for me. Correct answers will be approved.*"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Having some form of foundation is important. I believe creativity evolves from randomness, so this was the best approach I could think about. Now that both requirements were met, I needed to focus on uniqueness. People on the internet had already been creative for many years before I showed up, which meant that a lot of creations I came up with along this tiny journey were already taken on some platforms. And I had to cross-check every combination on every platform because I was not aware that there are websites that check it for you on all major asocial media."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 234,
"byteStart": 230
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 254,
"byteStart": 250
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "I came up with a decent number of prefixes and suffixes, which would not add any personal information and are essentially completely meaningless - further ensuring anonymity. As you probably already figured out by now, the prefix true and the suffix less were among them."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 141,
"byteStart": 128
},
"features": [
{
"uri": "https://trueberryless.org",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "And this is the whole unspectacular story about my little journey to create a unique, anonymous identifier across the internet: trueberryless."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreie233kmtghrqcnrozqqbtlteu4lajv5nmsz2wszhvowk2xu7vabgu"
},
"size": 324218,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "",
"publishedAt": "2024-12-12T19:45:00.000Z",
"publication": {
"url": "https://yappin.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreihbmln2jwabdtivgz4vz3snhf52mh4om7h5gqpnv4z6ueiostwm3q"
},
"size": 14608,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Yappin'",
"$type": "site.standard.publication",
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 43,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 250,
"g": 252,
"r": 253,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Just yappin' words 'cause therapy doesn't come with SEO.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Organising School Life: The Journey Behind Mutanuq
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mq36e6nxrk2l",
"data": {
"path": "/3mq36e6nxrk2l",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpyxzgvpnk2z",
"tags": [
"education",
"markdown",
"starlight"
],
"$type": "site.standard.document",
"title": "Organising School Life: The Journey Behind Mutanuq",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3db4-c952-7dda-b8d4-9c0f54fdf6c3",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "A vibrant back-to-school graphic featuring school supplies and cheerful colors, emphasizing the excitement of a new school year.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreidgcyyxlnlclzjc3gcgpanujesxjuncqhzv43vpo2in7ibldzqy2e"
},
"size": 457938,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 2400,
"height": 1596
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 350,
"byteStart": 341
},
"features": [
{
"uri": "https://witeboard.com/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "In my second year at HTL Krems, 2021, I embarked on a project that combined two things Iβve always been passionate about: creating my own website and finding better ways to organize the endless flow of school material. Like many students, my notes were scattered across OneNote, Word files, Excel sheets, Google Drive, and even tools like Witeboard. The chaos was overwhelming, making it difficult to focus on learning effectively."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Driven by the vision of streamlining this process β not just for me, but also for my classmates β I decided to create a centralized platform to summarize school content. My goal was twofold: simplify studying and inspire motivation among my peers."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 217,
"byteStart": 205
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "At that time, I wasnβt aware of frameworks that could make building documentation-style websites easier. Instead, I relied on plain HTML, CSS, and JavaScript to craft the first versions of what I called Schneider IT. Although I loved working on it, my early attempts were clunkyβgenerating the table of contents and sidebar dynamically with my own JavaScript code resulted in painfully slow load times. It took 10 seconds or more for everything to render!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Despite the setbacks, I kept iterating. By 2023, I had created and discarded about 10 versions of the site, always searching for a cleaner design and more efficient management system. The siteβs potential was clear to me, but its usability wasnβt there yet."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 39,
"byteStart": 30
},
"features": [
{
"uri": "https://starlight.astro.build",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 113,
"byteStart": 85
},
"features": [
{
"uri": "https://github.com/delucis",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Thatβs when I stumbled upon Starlight, a powerful and amazing framework created by Chris Swithinbank (@delucis) for building fast, Markdown-driven websites. Discovering Starlight was a turning point β it allowed me to blend two things Iβm passionate about: creating websites and using Markdown for its simplicity and elegance."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 67,
"byteStart": 60
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "With the transition to Starlight, Schneider IT evolved into Mutanuq. The new name marked a fresh start and reflected the streamlined, modernized website it had become. I rewrote everything, optimized the siteβs performance, and focused on creating content tailored specifically to my classmatesβ needs, especially for upcoming tests."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 142,
"byteStart": 108
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "The transformation paid off. Friends began using the website regularly and gave enthusiastic feedback like, \"Bro, this website looks awesome!\" Mutanuq wasnβt just a tool for exams β it became a trusted resource for my peers and a personal archive for all the knowledge I wanted to preserve beyond school."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 172,
"byteStart": 159
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
},
{
"index": {
"byteEnd": 236,
"byteStart": 229
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "Thanks to Starlight, managing and expanding Mutanuq became effortless, and the project grew into something Iβm proud to maintain even today. What started as Schneider IT β a small idea to organize my notes β blossomed into Mutanuq, a platform that simplifies learning for others while preserving valuable knowledge for my future self."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 83,
"byteStart": 50
},
"features": [
{
"uri": "https://mutanuq.trueberryless.org",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "You can take a look at Mutanuq under this domain: https://mutanuq.trueberryless.org"
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreidgcyyxlnlclzjc3gcgpanujesxjuncqhzv43vpo2in7ibldzqy2e"
},
"size": 457938,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "A fast, Markdown-powered website that organises school content, simplifies learning and supports educational growth.",
"publishedAt": "2023-11-13T18:53:00.000Z",
"publication": {
"url": "https://yappin.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreihbmln2jwabdtivgz4vz3snhf52mh4om7h5gqpnv4z6ueiostwm3q"
},
"size": 14608,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Yappin'",
"$type": "site.standard.publication",
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 43,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 250,
"g": 252,
"r": 253,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Just yappin' words 'cause therapy doesn't come with SEO.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Recommended Git Config
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mpvwzmluok2z",
"data": {
"path": "/3mpvwzmluok2z",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpts4vzcqc2e",
"tags": [
"git"
],
"$type": "site.standard.document",
"title": "Recommended Git Config",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f32fa-4d1f-7228-b348-123373b3327a",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "In order to optimize my git workflow, specifically for GitHub, I present you with some configs and tricks that I set up."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Automatic rebase"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 179,
"byteStart": 171
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 208,
"byteStart": 203
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 218,
"byteStart": 213
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 237,
"byteStart": 232
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 258,
"byteStart": 252
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Since I like to keep my commit history linear, I personally prefer to rebase instead of merge when I pull new changes from the remote repository. This means that if I run git pull, it will not perform a fetch and merge but rather a fetch followed by a rebase, which results in my local commits being automatically included on top of the remote commits coming in. That way, I avoid having merge commits that would IMHO pollute the history."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 54,
"byteStart": 46
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 72,
"byteStart": 68
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "If you want to try this out yourself, use the --rebase parameter on pull like that:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "git pull --rebase",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 117,
"byteStart": 109
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "If you feel happy with this workflow, you can set it in your global config, so you don't need to include the --rebase option anymore:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "git config --global pull.rebase true",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [
{
"index": {
"byteEnd": 20,
"byteStart": 16
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Default branch: main"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 62,
"byteStart": 56
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 166,
"byteStart": 162
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 227,
"byteStart": 219
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 257,
"byteStart": 253
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "By default, git often initializes new repositories with master as the primary branch. However, most modern platforms (especially GitHub) have moved towards using main. Instead of renaming your branch every time you run git init, you can tell git to use main globally by default:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "git config --global init.defaultBranch main",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Adding co-authors"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 187,
"byteStart": 178
},
"features": [
{
"uri": "https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/creating-a-commit-with-multiple-authors",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 284,
"byteStart": 278
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "I really like to credit people who contributed in any way to my commits. What I find annoying, however, is that you have to know the GitHub email address of the user you want to Co-author. I used to go to the people's profile, search for a recent commit they made and appending .patch to the GitHub commit URL. In the diff that you get this way, you can manually extract the email of the user. But why should I manually do this 1min work if I could spend 10h automating it?"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"gap": 20,
"$type": "pub.leaflet.blocks.imageGallery",
"format": "grid",
"images": [
{
"alt": "An xkcd comic strip titled 'Automation' exploring the discrepancy between theoretical expectation and reality. In the 'THEORY' panel, a graph shows a brief spike for 'WRITING CODE' before a sharp decline as 'AUTOMATION TAKES OVER,' leaving vast 'FREE TIME.' In the 'REALITY' panel, the 'WRITING CODE' spike leads into a fluctuating line of 'DEBUGGING' and 'RETHINKING' that eventually climbs steadily upward as 'ONGOING DEVELOPMENT,' leaving 'NO TIME FOR ORIGINAL TASK ANYMORE' as the workload permanently increases beyond the original baseline.",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreifxvnyqlelk25vmy3kgrc7nnsnbavlkir6kfe5l7rnbzkbz6cxhna"
},
"size": 27374,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 404,
"height": 408
}
},
{
"alt": "An xkcd comic titled 'Is It Worth the Time?' featuring a grid that calculates the time-saving break-even point over five years. The horizontal axis lists task frequency (50/day to yearly) and the vertical axis lists time saved per task (1 second to 1 day). Each cell shows the maximum time you should spend automating; for instance, saving 30 seconds on a daily task justifies 12 hours of work, while saving 1 second on a yearly task justifies only 5 seconds. The table uses various units like minutes, hours, days, weeks, and months to illustrate the limits of efficient automation.",
"$type": "pub.leaflet.blocks.imageGallery#image",
"image": {
"ref": {
"$link": "bafkreiahdyrsujrum4b3rrsd7h6jtj64cwzxc4sdqdlbfartiumikrp57a"
},
"size": 63370,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 571,
"height": 464
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 48,
"byteStart": 39
},
"features": [
{
"uri": "https://xkcd.com/1319/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 54,
"byteStart": 49
},
"features": [
{
"uri": "https://xkcd.com/1205/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Sorry, I just had to include those two legendary xkcds in here."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.horizontalRule"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 104,
"byteStart": 102
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 111,
"byteStart": 109
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 156,
"byteStart": 155
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f32fe-add6-7223-ae59-bc9b6dd67f51",
"contentPlaintext": "If you are on Windows, just use WSL please."
}
]
}
],
"plaintext": "So I created a little bash script which simplifies adding Co-authors to your commits. All you need is gh and jq installed, and the little bash script below*."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "brew install gh jq\n# sudo apt install gh jq",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 36,
"byteStart": 25
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 70,
"byteStart": 65
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 82,
"byteStart": 77
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 100,
"byteStart": 86
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 156,
"byteStart": 130
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Save this bash script as git-ucommit to a folder that is in your $PATH (like ~/bin or /usr/local/bin) and make it executable with chmod +x ~/bin/git-ucommit:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "#!/usr/bin/env bash\n\n# Check for dependencies\nif ! command -v gh &> /dev/null || ! command -v jq &> /dev/null; then\n echo \"Error: 'gh' (GitHub CLI) and 'jq' are required.\"\n exit 1\nfi\n\nargs=()\nusernames=()\nmessage=\"\"\n\n# Parse arguments\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n -u|--user)\n usernames+=(\"$2\")\n shift 2\n ;;\n -m|--message)\n message=\"$2\"\n shift 2\n ;;\n *)\n args+=(\"$1\")\n shift\n ;;\n esac\ndone\n\ntrailer_args=()\n\n# Resolve GitHub usernames to Co-authored-by trailers\nfor user in \"${usernames[@]}\"; do\n user_json=$(gh api users/\"$user\" 2>/dev/null)\n\n if [[ $? -ne 0 ]]; then\n echo \"Warning: Could not find GitHub user '$user'. Skipping.\"\n continue\n fi\n\n name=$(echo \"$user_json\" | jq -r '.name // .login')\n id=$(echo \"$user_json\" | jq -r '.id')\n email=\"$id+$user@users.noreply.github.com\"\n\n trailer_args+=(--trailer \"Co-authored-by: $name <$email>\")\ndone\n\n# Execute the commit\nif [[ -n \"$message\" ]]; then\n final_msg=$(echo -e \"$message\" | git interpret-trailers \"${trailer_args[@]}\")\n git commit \"${args[@]}\" -m \"$final_msg\"\nelse\n # No -m flag? Inject trailers and open the editor\n git commit \"${args[@]}\" \"${trailer_args[@]}\" -e\nfi",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 36,
"byteStart": 32
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 154,
"byteStart": 153
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f3300-4f97-7223-ae5f-9eed35cf6108",
"contentFacets": [
{
"index": {
"byteEnd": 77,
"byteStart": 71
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 114,
"byteStart": 113
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"contentPlaintext": "Unfortunately, I didn't find a way to add those options to the default commit command, so I prepended the letter u to it, which stands for users."
}
]
}
],
"plaintext": "Since the file name starts with git-, you do not even need to create a git alias, as it is smart enough to detect your executable and just allows you run*:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "git ucommit -m \"fix: suggestions\" -u trueberryless -u delucis",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 57,
"byteStart": 47
},
"features": [
{
"uri": "https://github.com/nix-darwin/nix-darwin",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 88,
"byteStart": 78
},
"features": [
{
"uri": "https://github.com/trueberryless/nix/blob/a1be76b4723b0061c34cca1063a477774156ce0a/modules/git-tools.nix",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "I myself integrated this functionality into my nix-darwin setup. Check out my Nix module if you can profit from this setup."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Summary"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 29,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "π Automatic Rebase on Pull"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"facets": [
{
"index": {
"byteEnd": 36,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "git config --global pull.rebase true"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"plaintext": "Sets your global configuration to perform a rebase instead of a merge when pulling. This keeps your git history linear and avoids unnecessary merge commits."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 26,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "π± Modern Default Branch"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"facets": [
{
"index": {
"byteEnd": 43,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "git config --global init.defaultBranch main"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"facets": [
{
"index": {
"byteEnd": 66,
"byteStart": 62
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 84,
"byteStart": 78
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Ensures every new local repository you initialize starts with main instead of master, aligning your local environment with GitHub's defaults."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 28,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "π₯ Simplified Co-Authoring"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"facets": [
{
"index": {
"byteEnd": 38,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "git ucommit -m \"message\" -u <username>"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"facets": [
{
"index": {
"byteEnd": 38,
"byteStart": 36
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 45,
"byteStart": 43
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Uses a custom bash script alongside gh and jq to automatically resolve GitHub usernames to the correct \"Co-authored-by\" trailers, saving you from manual email lookups."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Resources"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Here are some helpful resources for optimizing your Git and GitHub workflow:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 29,
"byteStart": 0
},
"features": [
{
"uri": "https://cli.github.com/manual/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "GitHub CLI (gh) Documentation"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 39,
"byteStart": 0
},
"features": [
{
"uri": "https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/creating-a-commit-with-multiple-authors",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Creating a commit with multiple authors"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 36,
"byteStart": 0
},
"features": [
{
"uri": "https://git-scm.com/docs/git-interpret-trailers",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Git Interpret Trailers Documentation"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "That's it! With these tweaks, your local environment stays modern, your history stays clean, and giving credit to your collaborators becomes a breeze. Happy coding!"
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreiexnwt5png4prz6lws2nva465zlgb6gvubqjp6ll33sw2gjmhqfnu"
},
"size": 794576,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "There are some quality of life configurations for git that can improve your workflow significantly.",
"publishedAt": "2026-03-08T19:59:00.000Z",
"publication": {
"url": "https://desired-state.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreigyqxqvi4ysi6q2rtnuaz6f37bj5ojfgpounuu36q57qvfbnb2fbq"
},
"size": 14046,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Desired State",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 219,
"g": 219,
"r": 219,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 768,
"accentText": {
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"accentBackground": {
"b": 0,
"g": 170,
"r": 222,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 0,
"g": 170,
"r": 222,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 0,
"g": 0,
"r": 0,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Clear, actionable guides on how to build, configure, and ship.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Uncommon Git Commands
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mpvwhvlubs2z",
"data": {
"path": "/3mpvwhvlubs2z",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpts4vzcqc2e",
"tags": [
"git"
],
"$type": "site.standard.document",
"title": "Uncommon Git Commands",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f32f4-132c-733c-af2f-54371a92f1ff",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 21,
"byteStart": 18
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "I like to use the git CLI. I have tried UIs like GitKraken, Fork or GitHub Desktop. I don't like them."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The majority of commands can be remembered after a short amount of time because you use them daily. However, there are some commands you only need here and there, which makes them really difficult to memorize. Every time I need them, I haven't used them for so long that I forget which option it was. Then I have to look them up again, feeling frustrated and uncool for not knowing Git by heart. It's embarrassing."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "So I decided to write this little post, which doesn't help memorizing them, but I'll always know where I can look them up quickly π
"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Checkout a remote branch on fork"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 281,
"byteStart": 271
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 423,
"byteStart": 419
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "When contributing to open source, it is common that you want to checkout a branch of the upstream branch, that doesn't exist in your remote fork yet. Neither does it exist in your local repository. Now when it comes to checking out a remote branch on a local repository, git switch is your friend. Just remember to fetch the upstream, so git knows the reference you want to track. However, what if the branch is not in your remote repository - your fork."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I usually have set up the original remote repository as the upstream repo locally, which can be done with:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "git remote add upstream https://github.com/<org>/<repo>",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "With this information, git can then fetch the upstream:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "git fetch upstream",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "And now comes the magic: Create and checkout a branch, that until then only exists in the original remote repo with:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "git checkout -b <branch> upstream/<branch>",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I know that it is pretty annoying that you have to specify the branch name two times, but in theory you could name the local branch differently."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Also don't forget that this branch does not yet exist in your remote fork, but you can push it easily with:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "git push -u origin <branch>",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Rename the commit message of an older commit"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "If you want to rename the last commit, just use:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "git commit --amend",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "When you already have multiple local commits, you have to start an interactive rebase with:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "git rebase -i HEAD~3",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Here, 3 is the number of commits you want to adapt. If your commit is back 10 commits, you need to put a 10 there instead."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Git will open your favourite editor with some text like:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "plaintext",
"plaintext": "pick 64e085f Commit Message 1\n\npick e5f607a Commit Message 2\n\npick a9b0c1d Commit Message 3",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 70,
"byteStart": 66
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 135,
"byteStart": 129
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 279,
"byteStart": 278
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f32f6-853d-7223-ae48-ba11fa3d41e6",
"contentPlaintext": "Renaming commits is not recommended when the commit is already pushed to the remote as you are changing the commit history for everyone."
}
]
}
],
"plaintext": "You can now edit this text like a normal file and replace all the pick keywords where you want to change the commit message with reword. After you save and close the file, git will repeatedly open text files with the old commit message, which you can freely edit to your likings*. After you adapted everything you wanted and close the last file, the rebase is finished and you can check your corrections with:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "git log --oneline",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 71,
"byteStart": 65
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 78,
"byteStart": 73
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 91,
"byteStart": 87
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Interactive rebasing is incredibly useful, as other options like squash, break or even exec are available. Just keep in mind that it makes more sense to do this on a branch where only you work to avoid team friction."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Clean up local branches"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 278,
"byteStart": 275
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 291,
"byteStart": 290
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f32f7-0e7a-7223-ae49-0831b5413ba2",
"contentFacets": [
{
"index": {
"byteEnd": 88,
"byteStart": 86
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 243,
"byteStart": 241
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"contentPlaintext": "This is a destructive action. Therefore I included a safeguard by using the lowercase -d parameter. If your branches are not cleanly merged, git will not delete them. If you are sure you want to delete all those branches no matter what, use -D instead."
}
]
}
],
"plaintext": "When a PR is merged, the branch is usually deleted after it is safely merged into the default branch. But the branch still exists locally. If you want to delete all the local branches, which got deleted in your remote repository, you can run this command in most shells (use wsl on Windows)*:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "git fetch -p && git branch -vv | awk '/: gone] / {print $1}' | grep -v '^\\*$' | xargs -I{} git branch -d \"{}\"",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [
{
"index": {
"byteEnd": 36,
"byteStart": 26
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Remove tracked files with .gitignore"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 48,
"byteStart": 37
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 58,
"byteStart": 54
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 202,
"byteStart": 192
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Weβve all been there: you create a config.json or a .env file, write some code, and commit it. Later, you realize that file should definitely not be in the repository, so you add it to your .gitignore."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 68,
"byteStart": 58
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 101,
"byteStart": 98
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
},
{
"index": {
"byteEnd": 225,
"byteStart": 217
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "The problem? Git is already tracking it. Adding a file to .gitignore only stops Git from tracking new files. To tell Git to \"forget\" a file it is already tracking without deleting it from your hard drive, you use the --cached flag:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "git rm --cached <file_path>",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 68,
"byteStart": 56
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 76,
"byteStart": 72
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "If you accidentally committed an entire directory (like node_modules or dist), you can do it recursively:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "git rm -r --cached <folder_name>/",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 140,
"byteStart": 130
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "If your repository has become a bit of a mess and you want to \"reset\" the tracking for everything to strictly follow your current .gitignore rules, run this sequence from the repository root:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "git rm -r --cached .\n\ngit add .\n\ngit commit -m \"Refactor: apply .gitignore to all tracked files\"",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 105,
"byteStart": 95
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 152,
"byteStart": 149
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 187,
"byteStart": 186
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f32f7-cd7f-7223-ae49-5afb1a7de525",
"contentPlaintext": "If the file contained secrets, untracking it is not enough. The secret is still present in earlier commits and may already be on the remote, so rotate the secret and consider rewriting history as well."
}
]
}
],
"plaintext": "This removes everything from the index and adds it all back. Because the files are now in your .gitignore, Git will simply skip over them during the add step, effectively untracking them*."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Summary"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 29,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "π°οΈ Track Upstream Branch"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"facets": [
{
"index": {
"byteEnd": 64,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "git fetch upstream && git checkout -b <branch> upstream/<branch>"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"plaintext": "Fetches the latest references from the original repository and creates a local branch that tracks the upstream version in one logical flow."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 25,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "π Rename Older Commits"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"facets": [
{
"index": {
"byteEnd": 20,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "git rebase -i HEAD~n"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"facets": [
{
"index": {
"byteEnd": 67,
"byteStart": 63
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 77,
"byteStart": 71
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 99,
"byteStart": 98
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Use this to rewrite history. In the interactive editor, change pick to reword for any of the last n commits you want to modify."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 25,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "π§Ή Prune Local Branches"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"facets": [
{
"index": {
"byteEnd": 109,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "git fetch -p && git branch -vv | awk '/: gone] / {print $1}' | grep -v '^\\*$' | xargs -I{} git branch -d \"{}\""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"plaintext": "A pipeline that identifies local branches whose remote counterparts have been deleted (\"gone\") and attempts to delete them safely."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 28,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "π§Ί Untrack \"Ignored\" Files"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"facets": [
{
"index": {
"byteEnd": 22,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "git rm --cached <file>"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"facets": [
{
"index": {
"byteEnd": 126,
"byteStart": 116
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Removes files from the Git index while keeping them physically on your disk. Essential for when you added a file to .gitignore after it was already committed."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Resources"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Here are some helpful resources for learning git and other situations you often find yourself in:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 33,
"byteStart": 0
},
"features": [
{
"uri": "https://learngitbranching.js.org/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "https://learngitbranching.js.org/"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 22,
"byteStart": 0
},
"features": [
{
"uri": "https://ohshitgit.com/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "https://ohshitgit.com/"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "That's it for now, more useful uncommon git commands might be added in the future!"
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreibieckexxfsdgne3lfql7rizj5pihlftiexjbs5zw76ym4mxlud7e"
},
"size": 1473580,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "This blog lists some git commands that can be pretty useful but are so rarely used, that you always forget them.",
"publishedAt": "2026-03-08T16:49:00.000Z",
"publication": {
"url": "https://desired-state.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreigyqxqvi4ysi6q2rtnuaz6f37bj5ojfgpounuu36q57qvfbnb2fbq"
},
"size": 14046,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Desired State",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 219,
"g": 219,
"r": 219,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 768,
"accentText": {
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"accentBackground": {
"b": 0,
"g": 170,
"r": 222,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 0,
"g": 170,
"r": 222,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 0,
"g": 0,
"r": 0,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Clear, actionable guides on how to build, configure, and ship.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Add Additional Content to Starlight's Table of Contents
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mpvkbvkhtc2u",
"data": {
"path": "/3mpvkbvkhtc2u",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpts4vzcqc2e",
"tags": [
"starlight"
],
"$type": "site.standard.document",
"title": "Add Additional Content to Starlight's Table of Contents",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3220-a2bf-7662-97c5-f3bf51bb52f3",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 148,
"byteStart": 139
},
"features": [
{
"uri": "https://starlight.astro.build",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "This guide shows how you can append additional content like credits, ads or additional information below to list of headings in your right Starlight sidebar."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Append content"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 59,
"byteStart": 49
},
"features": [
{
"uri": "https://starlight.astro.build/guides/overriding-components/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 85,
"byteStart": 64
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://starlight.astro.build/reference/overrides/#tableofcontents",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 95,
"byteStart": 85
},
"features": [
{
"uri": "https://starlight.astro.build/reference/overrides/#tableofcontents",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Starlight's table of contents can be modified by overriding the TableOfContents.astro component. Follow these steps to append a small Astro component after the existing table of contents:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.orderedList",
"children": [
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 25,
"byteStart": 15
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 179,
"byteStart": 164
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Create a small Card.astro component that will be displayed after the table of contents. If you want to follow common conventions, it is recommended to create a new src/components/ folder where you can place all your components:"
}
}
],
"startIndex": 1
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "astro",
"plaintext": "---\n// src/components/Card.astro\n---\n\n<aside class=\"not-content starlight-table-of-contents-aside\">\n <h2>Some card!</h2>\n <p>With some text π</p>\n</aside>\n\n<style>\n .starlight-table-of-contents-aside {\n position: relative;\n background-color: var(--sl-color-gray-6);\n padding: 1rem;\n border-radius: 0.5rem;\n border: 1px solid var(--sl-color-text-accent);\n box-shadow: var(--sl-shadow-md);\n overflow-y: hidden;\n }\n\n .starlight-table-of-contents-aside h2 {\n color: var(--sl-color-white);\n font-size: var(--sl-text-h5);\n font-weight: 600;\n text-decoration: none !important;\n line-height: var(--sl-line-height-headings);\n margin-bottom: 0.5rem;\n }\n\n .starlight-table-of-contents-aside p {\n font-size: var(--sl-text-xs);\n line-height: normal;\n text-decoration: none !important;\n color: var(--sl-color-gray-3);\n overflow-wrap: anywhere;\n }\n</style>",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.orderedList",
"children": [
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 32,
"byteStart": 11
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Create the TableOfContents.astro component, which will be used as the replacement of Starlight's default table of contents component:"
}
}
],
"startIndex": 2
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "astro",
"plaintext": "---\n// src/components/TableOfContents.astro\nimport Default from \"@astrojs/starlight/components/TableOfContents.astro\";\nimport Card from \"./Card.astro\";\n---\n\n<Default><slot /></Default>\n<div class=\"starlight-table-of-contents-wrapper-container\">\n <Card />\n</div>\n\n<style>\n .starlight-table-of-contents-wrapper-container {\n margin-top: 1rem;\n }\n</style>",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.orderedList",
"children": [
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Last but not least, do not forget to set the component override in your Astro config:"
}
}
],
"startIndex": 3
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "// astro.config.mjs\nimport { defineConfig } from 'astro/config';\nimport starlight from '@astrojs/starlight';\n\n// https://astro.build/config\nexport default defineConfig({\n integrations: [\n\tstarlight({\n \t title: 'My Docs',\n \t sidebar: [\n\t\t{\n \t\t label: 'Guide',\n \t\t items: [{ autogenerate: { directory: 'guide' } }],\n\t\t},\n \t ],\n \t components: {\n \t\tTableOfContents: \"./src/components/TableOfContents.astro\"\n \t }\n\t}),\n ],\n});",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Prepend content"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 47,
"byteStart": 42
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
},
{
"index": {
"byteEnd": 149,
"byteStart": 136
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "If you would prefer the table of contents after your component, you need to swap the order of the elements from Step 2 above and assign margin-bottom instead:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "astro",
"plaintext": "---\n\n// src/components/TableOfContents.astro\nimport Default from \"@astrojs/starlight/components/TableOfContents.astro\";\nimport Card from \"./Card.astro\";\n---\n\n<div class=\"starlight-table-of-contents-wrapper-container\">\n <Card />\n</div>\n<Default><slot /></Default>\n\n<style>\n .starlight-table-of-contents-wrapper-container {\n margin-bottom: 1rem;\n }\n</style>",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The rest of the 3 steps are the same, so do not forget to complete them all!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Place content at bottom"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 203,
"byteStart": 182
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "If you want to have more spacing between your new shiny content and the table of contents, you can use some CSS tricks to achieve this. Add these global styles to the bottom of your TableOfContents.astro component to introduce two new flexboxes which will transform your card to the bottom:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "astro",
"plaintext": "---\n// src/components/TableOfContents.astro\nimport Default from \"@astrojs/starlight/components/TableOfContents.astro\";\nimport Card from \"./Card.astro\";\n---\n\n<Default><slot /></Default>\n<div class=\"starlight-table-of-contents-wrapper-container\">\n <Card />\n</div>\n\n<style>\n .starlight-table-of-contents-wrapper-container {\n margin-top: 1rem;\n }\n</style>\n\n<style is:global>\n @media (min-width: 72rem) {\n .right-sidebar-panel {\n min-height: 100%;\n display: flex;\n flex-direction: column;\n }\n\n .right-sidebar-panel > .sl-container {\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n flex: 1;\n }\n }\n</style>",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Mobile support"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 25,
"byteStart": 4
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 155,
"byteStart": 128
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://starlight.astro.build/reference/overrides/#mobiletableofcontents",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 165,
"byteStart": 155
},
"features": [
{
"uri": "https://starlight.astro.build/reference/overrides/#mobiletableofcontents",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "The TableOfContents.astro is only rendered when the right sidebar is visible. This is not the case on mobile devices, where the MobileTableOfContents.astro component will be rendered."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 122,
"byteStart": 106
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://starlight.astro.build/reference/overrides/#pagination",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 132,
"byteStart": 122
},
"features": [
{
"uri": "https://starlight.astro.build/reference/overrides/#pagination",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "However, if you want to show your additional content on mobile devices, it is recommended to override the Pagination.astro component as adding content to the mobile view of the right sidebar would look out of place."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 65,
"byteStart": 49
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Here is a quick demonstration of how your custom Pagination.astro override could look like:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "astro",
"plaintext": "---\n// src/components/Pagination.astro\nimport Default from \"@astrojs/starlight/components/Pagination.astro\";\nimport Card from \"./Card.astro\";\n---\n\n<Default><slot /></Default>\n<div class=\"starlight-pagination-mobile-content\">\n <Card />\n</div>\n\n<style>\n .starlight-pagination-mobile-content {\n display: block;\n }\n @media (min-width: 72rem) {\n .starlight-pagination-mobile-content {\n display: none;\n }\n }\n</style>",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Note that the card will have much more width when displayed under the pagination than in the table of contents. So make sure to use responsive design inside your additional content."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Source code"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 75,
"byteStart": 69
},
"features": [
{
"uri": "https://github.com/trueberryless/starlight-customize-toc-additional-content",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 118,
"byteStart": 108
},
"features": [
{
"uri": "https://stackblitz.com/github/trueberryless/starlight-customize-toc-additional-content",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "You can also find the complete source code for all three variants on GitHub or open the project directly in StackBlitz."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreiabuygnpvuc6xgejevtziwjehf5d42rkb2feyg3qny5l7pfea4q2i"
},
"size": 425232,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "Learn how to append additional content like credits, ads or additional information below to list of headings in your right Starlight sidebar.",
"publishedAt": "2025-11-23T13:10:00.000Z",
"publication": {
"url": "https://desired-state.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreigyqxqvi4ysi6q2rtnuaz6f37bj5ojfgpounuu36q57qvfbnb2fbq"
},
"size": 14046,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Desired State",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 219,
"g": 219,
"r": 219,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 768,
"accentText": {
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"accentBackground": {
"b": 0,
"g": 170,
"r": 222,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 0,
"g": 170,
"r": 222,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 0,
"g": 0,
"r": 0,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Clear, actionable guides on how to build, configure, and ship.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Customize the "Overview" title of your Starlight Table of Contents
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mpvjajgyo22v",
"data": {
"path": "/3mpvjajgyo22v",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpts4vzcqc2e",
"tags": [
"starlight"
],
"$type": "site.standard.document",
"title": "Customize the \"Overview\" title of your Starlight Table of Contents",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3219-97e5-7cc5-adae-b035e198faef",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 116,
"byteStart": 107
},
"features": [
{
"uri": "https://starlight.astro.build",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "This guide shows how you can customize the first heading in the right sidebar (table of contents) of every Starlight page globally and individually."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 86,
"byteStart": 78
},
"features": [
{
"uri": "https://starlight.astro.build/reference/frontmatter/#tableofcontents",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 267,
"byteStart": 259
},
"features": [
{
"uri": "#global-definition",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 296,
"byteStart": 282
},
"features": [
{
"uri": "#frontmatter-override",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Each Starlight page contains a table of contents on the right side (except if disabled). To be able to link to the top of the page, the first entry of this sidebar has the title \"Overview\" (as you can also see on this page's sidebar), which can be customized globally and even on a per-page basis."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Global definition"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "If you want to set the title of the first table of contents entry once for all pages, you can follow these steps to achieve this for monolingual and multilingual sites:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.orderedList",
"children": [
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 23,
"byteStart": 7
},
"features": [
{
"uri": "https://starlight.astro.build/guides/route-data/#how-to-customize-route-data",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Create route middleware to customize the data shown by Starlight."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Use this example code to configure the \"Overview\" title:"
}
}
],
"startIndex": 1
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "// src/routeData.ts\nimport { defineRouteMiddleware } from \"@astrojs/starlight/route-data\";\n\nexport const onRequest = defineRouteMiddleware((context) => {\n const { starlightRoute } = context.locals;\n\n const overviewItem = starlightRoute.toc?.items[0];\n if (overviewItem) overviewItem.text = \"Back to top\";\n});",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.orderedList",
"children": [
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 28,
"byteStart": 16
},
"features": [
{
"uri": "https://starlight.astro.build/guides/i18n/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "If your site is multilingual, you can also assign different titles depending on the locale:"
}
}
],
"startIndex": 3
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "// src/routeData.ts\nimport { defineRouteMiddleware } from \"@astrojs/starlight/route-data\";\nimport starlightConfig from \"virtual:starlight/user-config\";\n\nconst defaultLang =\n starlightConfig.defaultLocale.lang ??\n starlightConfig.defaultLocale.locale ??\n \"en\";\n\nconst translations: Record<string, string> = {\n en: \"Back to top\",\n de: \"ZurΓΌck nach oben\",\n};\n\nexport const onRequest = defineRouteMiddleware((context) => {\n const { starlightRoute } = context.locals;\n const { locale } = starlightRoute;\n\n const overviewItem = starlightRoute.toc?.items[0];\n if (overviewItem) overviewItem.text = translations[locale ?? defaultLang];\n});",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Frontmatter override"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "In order to set the first table of contents heading to something different on individual pages, follow the steps below:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.orderedList",
"children": [
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 30,
"byteStart": 0
},
"features": [
{
"uri": "https://starlight.astro.build/reference/frontmatter/#customize-frontmatter-schema",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 55,
"byteStart": 34
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Extend your frontmatter schema in src/content.config.ts:"
}
}
],
"startIndex": 1
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "// src/content.config.ts\nimport { defineCollection, z } from 'astro:content';\nimport { docsLoader } from '@astrojs/starlight/loaders';\nimport { docsSchema } from '@astrojs/starlight/schema';\n\nexport const collections = {\n docs: defineCollection({\n loader: docsLoader(),\n schema: docsSchema({\n extend: z.object({\n overviewTitle: z.string().optional(),\n }),\n }),\n }),\n};",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.orderedList",
"children": [
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 40,
"byteStart": 27
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "This allows you to use the overviewTitle in your middleware like that:"
}
}
],
"startIndex": 2
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "// src/routeData.ts\nimport { defineRouteMiddleware } from \"@astrojs/starlight/route-data\";\n\nexport const onRequest = defineRouteMiddleware((context) => {\n const { starlightRoute } = context.locals;\n const individualOverviewTitle = starlightRoute.entry.data.overviewTitle;\n\n const overviewItem = starlightRoute.toc?.items[0];\n if (overviewItem)\n overviewItem.text = individualOverviewTitle ?? \"Back to top\";\n});",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "For the sake of completeness, here is a multilingual middleware that supports overriding on individual pages:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "// src/routeData.ts\nimport { defineRouteMiddleware } from \"@astrojs/starlight/route-data\";\nimport starlightConfig from \"virtual:starlight/user-config\";\n\nconst defaultLang =\n starlightConfig.defaultLocale.lang ??\n starlightConfig.defaultLocale.locale ??\n \"en\";\n\nconst translations: Record<string, string> = {\n en: \"Back to top\",\n de: \"ZurΓΌck nach oben\",\n};\n\nexport const onRequest = defineRouteMiddleware((context) => {\n const { starlightRoute } = context.locals;\n const { locale } = starlightRoute;\n const individualOverviewTitle = starlightRoute.entry.data.overviewTitle;\n const overviewItem = starlightRoute.toc?.items[0];\n if (overviewItem)\n overviewItem.text =\n individualOverviewTitle ?? translations[locale ?? defaultLang];\n});",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Source code"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 74,
"byteStart": 68
},
"features": [
{
"uri": "https://github.com/trueberryless/starlight-customize-toc-overview-title",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 117,
"byteStart": 107
},
"features": [
{
"uri": "https://stackblitz.com/github/trueberryless/starlight-customize-toc-overview-title",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "You can also find the complete source code for all four variants on GitHub or open the project directly in StackBlitz."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreicxdkssiftrcgnyu5tgus25tdyfwywi2utk2qldae62bnws5p2624"
},
"size": 822424,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "Learn how you can customize the first heading in the right sidebar of every Starlight page globally and individually.",
"publishedAt": "2025-11-22T12:52:00.000Z",
"publication": {
"url": "https://desired-state.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreigyqxqvi4ysi6q2rtnuaz6f37bj5ojfgpounuu36q57qvfbnb2fbq"
},
"size": 14046,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Desired State",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 219,
"g": 219,
"r": 219,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 768,
"accentText": {
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"accentBackground": {
"b": 0,
"g": 170,
"r": 222,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 0,
"g": 170,
"r": 222,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 0,
"g": 0,
"r": 0,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Clear, actionable guides on how to build, configure, and ship.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}How VS Code Snippets can improve your workflow when working with Astro
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mpvisgc65k2f",
"data": {
"path": "/3mpvisgc65k2f",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpts4vzcqc2e",
"tags": [
"astro",
"automation"
],
"$type": "site.standard.document",
"title": "How VS Code Snippets can improve your workflow when working with Astro",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3216-7ec4-7aa2-88b1-df559019e936",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 183,
"byteStart": 156
},
"features": [
{
"uri": "https://docs.astro.build/en/guides/content-collections",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "This article demonstrates how code snippets can be created in Visual Studio Code that can significantly boost your productivity when regularly working with Astro's content collections."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "The need for snippets"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 32,
"byteStart": 15
},
"features": [
{
"uri": "https://docs.astro.build/en/guides/content-collections/#defining-the-collection-schema",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 195,
"byteStart": 192
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 443,
"byteStart": 436
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Astro supports schema validation for your frontmatter fields in Markdown and MDX files. Therefore, many content files of your collections will have the same structure between the code fences (---). If you are tired of typing the same frontmatter fields over and over again, you can create a code snippet that will automatically generate the basic structure of the desired frontmatter while letting you conveniently edit the fields with TAB β₯ inputs."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Creating a snippet"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 157,
"byteStart": 154
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
},
{
"index": {
"byteEnd": 248,
"byteStart": 207
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 276,
"byteStart": 264
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 331,
"byteStart": 309
},
"features": [
{
"uri": "https://code.visualstudio.com/docs/editor/userdefinedsnippets#_create-your-own-snippets",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "You can create global snippets which can be scoped to multiple languages or language-specific snippets. Since we want the frontmatter snippet in Markdown and MDX files, we will create a global snippet under Code/User/snippets/markdown.code-snippets with the scope markdown,mdx. Follow the instructions in the official documentation for the correct steps for your Operating System."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 86,
"byteStart": 82
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 96,
"byteStart": 90
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Here is an example of how your snippet could look like. You can freely adjust the body or prefix as you see fit:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "json",
"plaintext": "{\n \"Add Markdown frontmatter\": {\n \"scope\": \"markdown,mdx\",\n \"prefix\": \"ffmt\",\n \"body\": [\n \"---\",\n \"title: $TM_FILENAME_BASE\",\n \"description: The description\",\n \"---\",\n \"\",\n \"## ${1:Heading}\",\n \"\",\n \"$0\"\n ],\n }\n}",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The flexibility of snippets allows you to create powerful and bespoke productivity boosters. You can enhance the basic configuration from above with the specific frontmatter fields you need, change how the title gets generated, and more."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Using the snippet"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 29,
"byteStart": 26
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 38,
"byteStart": 34
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 62,
"byteStart": 58
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 80,
"byteStart": 73
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 187,
"byteStart": 175
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 229,
"byteStart": 203
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Create an empty Markdown (.md) or .mdx file. Start typing ffmt and press TAB β₯ to trigger the snippet. If you do not see any suggestions, you can manually trigger them with Ctrl + Space and select the \"Add Markdown frontmatter\" snippet. Depending on your exact configuration, you can now tab through the fields to edit them to your current use case. For example the snippet from above allows you to directly edit the heading and then jumps to the content. The title field of the frontmatter will be set to the file name without its extension."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Screenshot of the Visual Studio Code editor, displaying generated content from a code snippet.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreiatzkfp2awsywom4hewdavysdc7nigk66h5ccb66xzojsxbql7xfq"
},
"size": 25694,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 954,
"height": 444
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Further resources"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 164,
"byteStart": 133
},
"features": [
{
"uri": "https://marketplace.visualstudio.com/items?itemName=SheltonLouis.astro-snippets",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "If you find snippets useful and want to explore a VS Code extension which includes a decent set of preconfigured snippets, check out Astro Snippets by Shelton Louis."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 79,
"byteStart": 66
},
"features": [
{
"uri": "https://hideoo.dev/notes",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Read more tips and tricks for working with Astro and Starlight on HiDeoo's blog."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreihat3jjppqajcdpyzyfo7ygml373lsla3qwl6xwdib5njjxlwbkc4"
},
"size": 2380636,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "Learn how to create and use VS Code snippets to quickly insert frontmatter for Astro content collections. This guide shows how snippets can automate repetitive tasks, speed up Markdown and MDX editing, and improve your Astro workflow.",
"publishedAt": "2025-10-28T12:44:00.000Z",
"publication": {
"url": "https://desired-state.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreigyqxqvi4ysi6q2rtnuaz6f37bj5ojfgpounuu36q57qvfbnb2fbq"
},
"size": 14046,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Desired State",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 219,
"g": 219,
"r": 219,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 768,
"accentText": {
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"accentBackground": {
"b": 0,
"g": 170,
"r": 222,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 0,
"g": 170,
"r": 222,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 0,
"g": 0,
"r": 0,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Clear, actionable guides on how to build, configure, and ship.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Autogenerated Starlight sidebar
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mpvilyrxoc2f",
"data": {
"path": "/3mpvilyrxoc2f",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpts4vzcqc2e",
"tags": [
"starlight"
],
"$type": "site.standard.document",
"title": "Autogenerated Starlight sidebar",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f320f-1b9e-7eec-b5f7-3a94ced6fec3",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 69,
"byteStart": 60
},
"features": [
{
"uri": "https://starlight.astro.build",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Have you ever wanted to simplify sidebar generation in your Starlight project? Have you tried autogenerating the entire sidebar only to find it doesn't let you customise the structure to your needs?"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "This post demonstrates two Starlight features that make fully autogenerated sidebars flexible and reduce maintenance."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Use case"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 137,
"byteStart": 106
},
"features": [
{
"uri": "https://starlight.astro.build/reference/configuration/#sidebar",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "In medium to large Starlight projects, the problem arises that the manual creation and maintenance of the Starlight sidebar configuration is a tedious and time-consuming task."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 288,
"byteStart": 278
},
"features": [
{
"uri": "https://starlight.astro.build/guides/route-data/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 321,
"byteStart": 309
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://starlight.astro.build/reference/configuration/#generateid",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 330,
"byteStart": 321
},
"features": [
{
"uri": "https://starlight.astro.build/reference/configuration/#generateid",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 340,
"byteStart": 330
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://starlight.astro.build/reference/configuration/#generateid",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "In the past, Starlight didn't provide the right primitives for this, and the recommended approach was to move the autogeneration of the sidebar configuration one level deeper - so that only folders without nested subfolders were autogenerated. However, with the introduction of Route Data and the more recent generateId() hook of docsLoader, these scenarios can now be solved much more easily and efficiently."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Solving the ordering issue"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 132,
"byteStart": 127
},
"features": [
{
"uri": "https://github.com/withastro/starlight/issues/1223",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 176,
"byteStart": 164
},
"features": [
{
"uri": "https://content.nuxt.com",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 250,
"byteStart": 222
},
"features": [
{
"uri": "https://v2.content.nuxt.com/usage/content-directory#ordering",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "A common problem always was: \"How can I customise the order of sidebar folders if I autogenerate the sidebar?\" Old issues like #1223 suggest a solution inspired by Nuxt Content where each segment of the whole route can be prefixed with numbers + dots to determine numeric ordering on a file-based level. An example for such a project could look like this:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "src/content/docs/nested-folder/\nβββ 1.frameworks/\nβ βββ 1.vue.md\nβ βββ 2.nuxt.md\nβββ 2.examples/\n βββ 1.vercel.md\n βββ 2.netlify.md",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "If you want to use this approach in Starlight, it doesn't work out-of-the-box, but with a simple setup you can make it work."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 16,
"byteStart": 10
},
"features": [
{
"uri": "https://github.com/withastro/starlight/releases/tag/%40astrojs%2Fstarlight%400.35.0",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 82,
"byteStart": 70
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 168,
"byteStart": 165
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Starlight 0.35.0 introduced a callback function which you can pass to docsLoader() that can manipulate the id (URL) of the generated page. By splitting the entry by \"/\" and removing any number + dot on the segment level, you have the same functionality now in Starlight:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "// src/content.config.ts\nimport { defineCollection } from \"astro:content\";\nimport { docsLoader } from \"@astrojs/starlight/loaders\";\nimport { docsSchema } from \"@astrojs/starlight/schema\";\n\nconst leadingNumberAndDotRegEx = /^\\d+\\./;\nconst fileExtensionRegEx = /\\.(md|mdx)$/;\n\nexport const collections = {\n docs: defineCollection({\n loader: docsLoader({\n // Remove file extension and\n // leading numbers + dot from each segment\n generateId: ({ entry }) =>\n entry\n .replace(fileExtensionRegEx, \"\")\n .split(\"/\")\n .map((segment) => segment.replace(leadingNumberAndDotRegEx, \"\"))\n .join(\"/\"),\n }),\n schema: docsSchema(),\n }),\n};",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 84,
"byteStart": 51
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 117,
"byteStart": 88
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "The URL to access the pages now changes e.g., from /nested-folder/1.frameworks/1.vue to /nested-folder/frameworks/vue."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 61,
"byteStart": 59
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "However, the folder names in the sidebar still include the 1. prefixes. So let's solve the sidebar manipulation as well:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Solving the sidebar folder naming issue"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 16,
"byteStart": 10
},
"features": [
{
"uri": "https://github.com/withastro/starlight/releases/tag/%40astrojs%2Fstarlight%400.32.0",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 38,
"byteStart": 28
},
"features": [
{
"uri": "https://starlight.astro.build/guides/route-data/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Starlight 0.32.0 introduced Route Data, which we can now utilize to manipulate folder names in the sidebar to"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.orderedList",
"children": [
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "remove any leading number + dot from the folder name and"
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 59,
"byteStart": 58
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f3213-f4aa-755f-ab7b-1c066d78d3c7",
"contentFacets": [
{
"index": {
"byteEnd": 53,
"byteStart": 10
},
"features": [
{
"uri": "https://starlight.astro.build/guides/sidebar/#internationalization",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 288,
"byteStart": 282
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://starlight.astro.build/reference/route-data/#locale",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 314,
"byteStart": 288
},
"features": [
{
"uri": "https://starlight.astro.build/reference/route-data/#locale",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 346,
"byteStart": 342
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 384,
"byteStart": 365
},
"features": [
{
"uri": "#using-hideoos-plugin",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 405,
"byteStart": 392
},
"features": [
{
"uri": "https://starlight-auto-sidebar.netlify.app/guides/i18n/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"contentPlaintext": "Note that internationalisation (i18n) of folder names is a missing feature with such a setup as there is currently nowhere to define translations of folder names, they just get converted into title case. Implementing logic to dynamically load translations could involve reading the locale field from the route data and define translations in json files. Otherwise, use HiDeoo's plugin, which supports i18n."
}
]
}
],
"plaintext": "apply \"Title Case\" so they don't look completely lowercase*"
}
}
],
"startIndex": 1
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "// src/routeData.ts\nimport { defineRouteMiddleware } from \"@astrojs/starlight/route-data\";\n\nconst leadingNumberAndDotRegEx = /^\\d+\\./;\nconst wordSplitterRegEx = /\\w\\S*/g;\n\nfunction toTitleCase(str: string) {\n return str.replace(wordSplitterRegEx, (word) => {\n return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();\n });\n}\n\nfunction cleanGroupLabels(entries: any[]) {\n for (const entry of entries) {\n if (entry.type === \"group\") {\n // Remove leading number + dot\n let label = entry.label.replace(leadingNumberAndDotRegEx, \"\");\n // Convert to Title Case\n entry.label = toTitleCase(label);\n // Recurse into children\n cleanGroupLabels(entry.entries);\n }\n }\n}\n\nexport const onRequest = defineRouteMiddleware((context) => {\n const { sidebar } = context.locals.starlightRoute;\n cleanGroupLabels(sidebar);\n});",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 36,
"byteStart": 24
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 66,
"byteStart": 50
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Don't forget to add the routeData.ts file to your astro.config.mjs:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "// astro.config.mjs\n starlight({\n title: \"Autogenerate whole sidebar\",\n sidebar: [\n {\n label: \"Root Folder\",\n autogenerate: { directory: \"nested-folder\" },\n },\n ],\n routeMiddleware: \"./src/routeData.ts\",\n }),",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Of course you can adjust the code to your needs (e.g., remove the \"Title Case\" transformation because your file names are already written correctly)."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Using HiDeoo's plugin"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 118,
"byteStart": 112
},
"features": [
{
"uri": "https://hideoo.dev",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 190,
"byteStart": 168
},
"features": [
{
"uri": "https://starlight-auto-sidebar.netlify.app",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "If you want to autogenerate your whole sidebar and still have the flexibility to customise everything you want, HiDeoo also made a dedicated plugin for such use cases: Starlight Auto Sidebar."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Especially those two features got mentioned in this article:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 21,
"byteStart": 16
},
"features": [
{
"uri": "https://starlight-auto-sidebar.netlify.app/metadata/#order",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Controlling the order of individual directories"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 28,
"byteStart": 13
},
"features": [
{
"uri": "https://starlight-auto-sidebar.netlify.app/metadata/#label",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Adapting the displayed label of individual folders"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "However, if you want to automate the whole generation without having to specify labels and orders manually for each directory, coding the solution yourself (with some guidelines in this blog) is the preferred and recommended solution."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 91,
"byteStart": 81
},
"features": [
{
"uri": "https://stackblitz.com/github/trueberryless/starlight-autogenerate-whole-sidebar?file=src%2Fcontent.config.ts",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 129,
"byteStart": 118
},
"features": [
{
"uri": "https://github.com/trueberryless/starlight-autogenerate-whole-sidebar",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "If you want to check out the code presented in this blog, feel free to visit the StackBlitz or the source code in the GitHub repo."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreiekisb2nkhv6wp26ebwmnmufxele722hhugnvgoglieobxdjalkcu"
},
"size": 477114,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "Learn two Starlight techniques to autogenerate large sidebars while keeping clean URLs and labels.",
"publishedAt": "2025-08-17T11:41:00.000Z",
"publication": {
"url": "https://desired-state.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreigyqxqvi4ysi6q2rtnuaz6f37bj5ojfgpounuu36q57qvfbnb2fbq"
},
"size": 14046,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Desired State",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 219,
"g": 219,
"r": 219,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 768,
"accentText": {
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"accentBackground": {
"b": 0,
"g": 170,
"r": 222,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 0,
"g": 170,
"r": 222,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 0,
"g": 0,
"r": 0,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Clear, actionable guides on how to build, configure, and ship.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}SΓ€tteri plugin that turns GitHub links into badges
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mpvhzjwjlk2a",
"data": {
"path": "/3mpvhzjwjlk2a",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpts4vzcqc2e",
"tags": [
"plugins",
"sΓ€tteri"
],
"$type": "site.standard.document",
"title": "SΓ€tteri plugin that turns GitHub links into badges",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f31f9-1a3f-7441-8326-ede13ae36cdc",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 50,
"byteStart": 35
},
"features": [
{
"uri": "https://satteri.bruits.org/docs/plugins/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "This post explains how to create a SΓ€tteri plugin to transform Markdown links that link to GitHub users or organisations into beautiful badges."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 72,
"byteStart": 63
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mpvgufpwwk2m",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "If you are looking for the equivalent Rehype plugin, check out this post."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Create the plugin code"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "This is the entire code for the SΓ€tteri plugin:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "import { defineHastPlugin } from \"satteri\";\n\nexport function satteriGitHubBadgeLinks() {\n return defineHastPlugin({\n name: \"github-badge-links\",\n element: {\n filter: [\"a\"],\n visit(node, ctx) {\n const href = node.properties.href;\n if (\n typeof href === \"string\" &&\n href.startsWith(\"https://github.com/\")\n ) {\n const match = href.match(\n /^https:\\/\\/github\\.com\\/([\\w-]+)\\/?$/\n );\n\n if (match) {\n const username = match[1];\n const currentClassName = Array.isArray(node.properties.className)\n ? node.properties.className\n : [];\n ctx.setProperty(node, \"className\", currentClassName.concat(\"gh-badge\"));\n\n const avatarImg = {\n type: \"element\",\n tagName: \"img\",\n properties: {\n src: `https://github.com/${username}.png`,\n alt: username,\n },\n children: [],\n };\n ctx.prependChild(node, avatarImg);\n }\n }\n },\n },\n });\n}",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 169,
"byteStart": 164
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Basically all this plugin does, is hook into the SΓ€tteri pipeline through its APIs to traverse all link elements. If it finds one that links to GitHub, it adds an <img> tag before the text content with the profile picture of the GitHub user or organisation."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Add some CSS"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "In order to make the newly added image look beautiful on your website, add this little snippet of CSS. Feel free to adjust it to your needs."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "css",
"plaintext": ".gh-badge {\n display: inline-flex;\n align-items: center;\n background-color: #21262d;\n border-radius: 9999px;\n padding: 0em 0.5em 0 0.3em;\n font-size: 0.9em;\n text-decoration: none;\n color: #f0f6fc;\n font-weight: 500;\n transition: background-color 0.2s ease, border-color 0.2s ease;\n transform: translateY(0.29rem);\n border: 1px solid #30363d;\n}\n\n.gh-badge:hover {\n background-color: #30363d;\n border-color: #8b949e;\n}\n\n.gh-badge img {\n border-radius: 9999px;\n width: 1.3em;\n height: 1.3em;\n}",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Hook into the pipeline"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 118,
"byteStart": 108
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "That's it! Now you just need to hook your plugin into SΓ€tteri's MDAST to HAST pipeline by defining it as a hastPlugin:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "import { markdownToHtml } from \"satteri\";\nimport { satteriGitHubBadgeLinks } from \"./src/lib/satteri-github-badge-links.ts\";\n\nmarkdownToHtml(source, {\n hastPlugins: [satteriGitHubBadgeLinks()],\n});",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Example: Astro"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 162,
"byteStart": 146
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "If you are building your website with Astro and want to use this SΓ€tteri plugin, you can hook the plugin to SΓ€tteri's pipeline directly in your astro.config.mjs:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "import { defineConfig } from 'astro/config';\nimport { satteri } from '@astrojs/markdown-satteri';\nimport { satteriGitHubBadgeLinks } from './src/lib/satteri-github-badge-links.ts';\n\nexport default defineConfig({\n markdown: {\n processor: satteri({\n hastPlugins: [satteriGitHubBadgeLinks()],\n }),\n },\n});",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 59,
"byteStart": 34
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Just do not forget to install the @astrojs/markdown-satteri package explicitly."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 76,
"byteStart": 59
},
"features": [
{
"uri": "https://docs.astro.build/en/guides/markdown-content/#using-s%C3%A4tteri-plugins-and-features",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Read more about how to integrate this plugin into Astro in its documentation."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreiab64dzqhe7ctm4n252emr4w4azkwhvi53sv237bq2bdamqjhlfsa"
},
"size": 841896,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "Learn how to build a simple yet delightful SΓ€tteri plugin that automatically transforms plain GitHub profile links into stylish, badge-like links with profile images.",
"publishedAt": "2026-07-05T11:31:12.944Z",
"publication": {
"url": "https://desired-state.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreigyqxqvi4ysi6q2rtnuaz6f37bj5ojfgpounuu36q57qvfbnb2fbq"
},
"size": 14046,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Desired State",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 219,
"g": 219,
"r": 219,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 768,
"accentText": {
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"accentBackground": {
"b": 0,
"g": 170,
"r": 222,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 0,
"g": 170,
"r": 222,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 0,
"g": 0,
"r": 0,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Clear, actionable guides on how to build, configure, and ship.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Rehype plugin that turns GitHub links into badges
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mpvgufpwwk2m",
"data": {
"path": "/3mpvgufpwwk2m",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpts4vzcqc2e",
"tags": [
"plugins",
"rehype"
],
"$type": "site.standard.document",
"title": "Rehype plugin that turns GitHub links into badges",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f31d4-07f1-711c-b56a-7cdf3da05ca7",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 48,
"byteStart": 35
},
"features": [
{
"uri": "https://github.com/rehypejs",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "This post explains how to create a Rehype plugin to transform Markdown links that link to GitHub users or organisations into beautiful badges."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 71,
"byteStart": 62
},
"features": [
{
"did": "did:plc:2pdiyh6lip2aomv7kia3f2jo",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
}
],
"plaintext": "The inspiration for the design was taken from none other than @antfu.me himself, as they have those badges all over the place on their website (as time of writing)."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 89,
"byteStart": 80
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mpvhzjwjlk2a",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "If you are looking for the equivalent SΓ€tteri plugin (for Astro 7+), check out this post."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "This is the whole code behind the Rehype plugin:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "// src/lib/rehype-github-badge-links.ts\nimport { h } from \"hastscript\";\nimport { visit } from \"unist-util-visit\";\n\nexport default function rehypeGitHubBadgeLinks() {\n return (tree) => {\n visit(tree, \"element\", (node) => {\n if (\n node.tagName === \"a\" &&\n typeof node.properties?.href === \"string\" &&\n node.properties.href.startsWith(\"https://github.com/\")\n ) {\n const match = node.properties.href.match(\n /^https:\\/\\/github\\.com\\/([\\w-]+)\\/?$/\n );\n if (match) {\n const username = match[1];\n // Add GitHub badge class\n node.properties.className = (node.properties.className || []).concat(\n \"gh-badge\"\n );\n\n // Build avatar image\n const avatarImg = h(\"img\", {\n src: `https://github.com/${username}.png`,\n alt: username,\n });\n // Prepend avatar image to original children\n node.children.unshift(avatarImg);\n }\n }\n });\n };\n}",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 151,
"byteStart": 146
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 391,
"byteStart": 387
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 449,
"byteStart": 425
},
"features": [
{
"uri": "https://dev.to/10xlearner/how-to-get-the-profile-picture-of-a-github-account-1d82",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 455,
"byteStart": 449
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://dev.to/10xlearner/how-to-get-the-profile-picture-of-a-github-account-1d82",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Basically all this plugin does, is walking through the HTML, looking for links which reference to any GitHub profile. If it finds one, it adds an <img> tag before the text content with the profile picture of the GitHub user or organisation. This is possible very consistently thanks to GitHub's feature of making the picture available as a resource behind the profile link appended with .png. Read more about that feature in this awesome article on dev.to."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 76,
"byteStart": 75
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f31d9-8086-788c-98e3-c1f79a81eae8",
"contentFacets": [
{
"index": {
"byteEnd": 56,
"byteStart": 47
},
"features": [
{
"uri": "https://starlight.astro.build",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"contentPlaintext": "Please note that since I made the badges for a StarlightΒ site, I use its CSS variables. You can play around with colours as you like."
}
]
}
],
"plaintext": "With a little bit of additional styling it looks really cute in my opinion.* "
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "css",
"plaintext": ".gh-badge {\n display: inline-flex;\n align-items: center;\n background-color: var(--sl-color-accent-low);\n border-radius: 9999px;\n padding: 0em 0.5em 0 0.3em;\n font-size: 0.9em;\n text-decoration: none;\n color: inherit;\n font-weight: 500;\n transition: background-color 0.2s ease;\n transform: translateY(0.29rem);\n border: 1px solid var(--sl-color-accent);\n}\n\n.gh-badge:hover {\n background-color: var(--sl-color-accent);\n}\n\n.gh-badge img {\n border-radius: 9999px;\n width: 1.3em;\n height: 1.3em;\n}",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Now to put everything together, let's say for example in an Astro site, you just need to add the Rehype plugin to the configuration like this:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "// astro.config.mjs\nimport { defineConfig } from 'astro/config';\nimport rehypeGitHubBadgeLinks from \"./src/lib/rehype-github-badge-links\";\n\nexport default defineConfig({\n markdown: {\n rehypePlugins: [rehypeGitHubBadgeLinks]\n }\n})",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 89,
"byteStart": 60
},
"features": [
{
"uri": "https://docs.astro.build/en/reference/configuration-reference/#markdownrehypeplugins",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Read more about the injection of Rehype plugins in Astro in their configuration reference."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 160,
"byteStart": 142
},
"features": [
{
"uri": "https://docs.astro.build/en/guides/styling/#scoped-styles",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Do not forget to add the CSS in a similar way depending on your framework - in Starlight you can configure custom global CSS styles following these instructions - and you can admire your own badge links too. Feel free to share this post with anyone you want to persuade to use these features too."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreihmtboj37gg7ywxgdowunz4xzjtakxh4t5n5j7k7fz6kefptg2rfi"
},
"size": 622818,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "Learn how to build a simple yet delightful Rehype plugin that automatically transforms plain GitHub profile links into stylish, badge-like links with profile images β perfect for adding personality to your blog or docs.",
"publishedAt": "2025-07-12T11:10:00.000Z",
"publication": {
"url": "https://desired-state.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreigyqxqvi4ysi6q2rtnuaz6f37bj5ojfgpounuu36q57qvfbnb2fbq"
},
"size": 14046,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Desired State",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 219,
"g": 219,
"r": 219,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 768,
"accentText": {
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"accentBackground": {
"b": 0,
"g": 170,
"r": 222,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 0,
"g": 170,
"r": 222,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 0,
"g": 0,
"r": 0,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Clear, actionable guides on how to build, configure, and ship.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Horizontal progress indicator for your Starlight site
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mpvd3fc4ss23",
"data": {
"path": "/3mpvd3fc4ss23",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpts4vzcqc2e",
"tags": [
"starlight",
"css"
],
"$type": "site.standard.document",
"title": "Horizontal progress indicator for your Starlight site",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f31b7-298f-7ee7-86d6-3f6a1a160469",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 53,
"byteStart": 44
},
"features": [
{
"uri": "https://github.com/frostybee",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "In this post, we would like to show you how FrostyBee created a clean horizontal progress indicator for Starlight sites."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Prerequisites"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "You will need to have an existing Starlight website."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Create the progress indicator component"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 59,
"byteStart": 36
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 82,
"byteStart": 67
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Let's create a new component called ProgressIndicator.astro in the src/components/ directory. This component will be responsible for rendering the progress indicator."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 84,
"byteStart": 79
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 121,
"byteStart": 116
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "We create a container which spans across the whole width with the progress bar <div> inside. The width of the inner <div> is the updated whenever the user scrolls (and when the page loads) to reflect the current scroll position."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "astro",
"plaintext": "<div class=\"progress-scroll-container\" aria-hidden=\"true\">\n <div id=\"progress-scroll\"></div>\n</div>\n\n<script>\n window.addEventListener(\"scroll\", function () {\n updateProgressScroll();\n });\n window.addEventListener(\"load\", function () {\n updateProgressScroll();\n });\n function updateProgressScroll() {\n if (document) {\n const progressScroll = document.getElementById(\"progress-scroll\");\n if (progressScroll) {\n var scrollTop = window.scrollY || document.documentElement.scrollTop;\n var scrollHeight =\n document.documentElement.scrollHeight - window.innerHeight;\n var progress = (scrollTop / scrollHeight) * 100;\n progressScroll.style.width = progress + \"%\";\n }\n }\n }\n</script>\n\n<style>\n html:is(:not([data-has-hero]), [data-has-sidebar], [data-has-toc]) .progress-scroll-container {\n position: fixed;\n top: var(--sl-nav-height);\n left: 0;\n height: 0.25rem;\n width: 100%;\n background-color: transparent;\n z-index: 3;\n }\n #progress-scroll {\n height: 100%;\n width: 0px;\n background-color: var(--sl-color-accent-high);\n }\n</style>\n\n<style is:global>\n #starlight__on-this-page--mobile {\n margin-top: 0.25rem;\n }\n</style>",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "In the hidden lines is the styling which makes sure that the progress bar looks like a typical progress bar. It is also suited to Starlight because the bar is automatically hidden when the site has no sidebar and table of contents, but a hero element as most landing pages do."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "One thing to point out is what the global CSS (line 45 - almost at the end of the file) does: It pushes the mobile table of contents dropdown menu down to create more whitespace to the progress bar. This is a personal preference and you can remove it if you want."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Starlight Override"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 53,
"byteStart": 35
},
"features": [
{
"uri": "https://starlight.astro.build/guides/overriding-components/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 111,
"byteStart": 99
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://starlight.astro.build/reference/overrides/#header-1",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 121,
"byteStart": 111
},
"features": [
{
"uri": "https://starlight.astro.build/reference/overrides/#header-1",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Now we can use this component in a Starlight override. In this case it makes sense to override the Header.astro component like this:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "astro",
"plaintext": "---\nimport Default from \"@astrojs/starlight/components/Header.astro\";\nimport ScrollIndicator from \"./ScrollIndicator.astro\";\n---\n\n<Default><slot /></Default>\n<ScrollIndicator />",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 69,
"byteStart": 53
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Do not forget to add this component override to your astro.config.mjs file:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "import { defineConfig } from 'astro/config';\nimport starlight from '@astrojs/starlight';\n\nexport default defineConfig({\n integrations: [\n starlight({\n title: 'My Docs with a Progress Indicator',\n components: {\n // Override the default `Header` component.\n Header: './src/components/Header.astro',\n },\n }),\n ],\n});",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Source Code"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 45,
"byteStart": 39
},
"features": [
{
"uri": "https://github.com/trueberryless/starlight-progress-indicator",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 76,
"byteStart": 53
},
"features": [
{
"uri": "https://stackblitz.com/github/trueberryless/starlight-progress-indicator?file=src%2Fcomponents%2FProgressIndicator.astro",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "You can find the whole project code on GitHub and in this Stackblitz example."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Conclusion"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "With this setup, the progress indicator will be displayed on every page (except landing pages) of your Starlight site. π₯³"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 34,
"byteStart": 23
},
"features": [
{
"uri": "https://github.com/frostybee",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 65,
"byteStart": 35
},
"features": [
{
"uri": "https://github.com/frostybee/starlight-scroll-to-top",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 110,
"byteStart": 97
},
"features": [
{
"uri": "https://frostybee.github.io/starlight-scroll-to-top/configuration/#showprogressring",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 130,
"byteStart": 125
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "You can also check out FrostyBee's Starlight Scroll To Top plugin which also supports a built in Progress Ring since version 0.3.1. π"
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreidxeoughxztu2au2gnqoi5anhf7pnjkrp6kk3yxe5kvdmtbxe6kxy"
},
"size": 38644,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "Learn how to create a horizontal progress indicator for your Starlight site.",
"publishedAt": "2025-06-10T10:02:00.000Z",
"publication": {
"url": "https://desired-state.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreigyqxqvi4ysi6q2rtnuaz6f37bj5ojfgpounuu36q57qvfbnb2fbq"
},
"size": 14046,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Desired State",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 219,
"g": 219,
"r": 219,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 768,
"accentText": {
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"accentBackground": {
"b": 0,
"g": 170,
"r": 222,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 0,
"g": 170,
"r": 222,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 0,
"g": 0,
"r": 0,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Clear, actionable guides on how to build, configure, and ship.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Starlight Sidebar Topics Dropdown and List together
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mpvcsbo2tk2x",
"data": {
"path": "/3mpvcsbo2tk2x",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpts4vzcqc2e",
"tags": [
"starlight",
"css",
"plugins"
],
"$type": "site.standard.document",
"title": "Starlight Sidebar Topics Dropdown and List together",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f31ae-5e68-7ee7-86d5-f785d7c7a285",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 34,
"byteStart": 10
},
"features": [
{
"uri": "https://github.com/hideoo/starlight-sidebar-topics",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 93,
"byteStart": 60
},
"features": [
{
"uri": "https://github.com/trueberryless-org/starlight-sidebar-topics-dropdown",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Using the Starlight Sidebar Topics plugin together with the Starlight Sidebar Topics Dropdown component, you can create a website that has a list of topics on desktop and a dropdown menu on mobile."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Prerequisites"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "You will need to have an existing Starlight website."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Install the packages"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 43,
"byteStart": 19
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 97,
"byteStart": 64
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "First, install the starlight-sidebar-topics plugin and also the starlight-sidebar-topics-dropdown component:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "npm install starlight-sidebar-topics starlight-sidebar-topics-dropdown\n# pnpm add starlight-sidebar-topics starlight-sidebar-topics-dropdown\n# yarn add starlight-sidebar-topics starlight-sidebar-topics-dropdown",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 65,
"byteStart": 41
},
"features": [
{
"uri": "https://starlight-sidebar-topics.trueberryless.org/docs/getting-started/#installation",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 103,
"byteStart": 70
},
"features": [
{
"uri": "https://starlight-sidebar-topics-dropdown.trueberryless.org/docs/getting-started/#installation",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Afterwards, follow the setup guides from Starlight Sidebar Topics and Starlight Sidebar Topics Dropdown."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Modify the Sidebar component"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 180,
"byteStart": 152
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "In the setup guide from the dropdown component, you have created a sidebar component, which only renders the dropdown menu. Now, you need to modify the src/components/Sidebar.astro component to also render the default sidebar if the user is on desktop."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "astro",
"plaintext": "---\nimport Default from '@astrojs/starlight/components/Sidebar.astro';\nimport Topics from 'starlight-sidebar-topics/components/Sidebar.astro';\nimport TopicsDropdown from 'starlight-sidebar-topics-dropdown/TopicsDropdown.astro';\n---\n\n<div class=\"topics\">\n <Topics/>\n</div>\n<div class=\"topics-dropdown\">\n <TopicsDropdown/>\n</div>\n<Default><slot /></Default>\n\n<style>\n /* Hide topics by default and show them on medium screens and larger */\n .topics {\n display: none;\n }\n\n @media (min-width: 800px) {\n .topics {\n display: block;\n }\n }\n\n /* Show topics dropdown on small screens and hide it on medium screens and larger */\n .topics-dropdown {\n display: block;\n }\n\n @media (min-width: 800px) {\n .topics-dropdown {\n display: none;\n }\n }\n</style>",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Result"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "If you follow these steps, your sidebar will look like this:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Final result with desktop view on the left and mobile view on the right",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreicg4nyothzw735qba6hd5mtfkhmru56qzlbxvwe5cluyov7rpeavq"
},
"size": 14890,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 652,
"height": 352
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 78,
"byteStart": 60
},
"features": [
{
"uri": "https://stackblitz.com/edit/withastro-starlight-qgraahmp?file=astro.config.mjs",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "You can find the complete source code of this guide in this StackBlitz example."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Endless possibilities"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 110,
"byteStart": 96
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 128,
"byteStart": 115
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "You could also do it the other way around (list on mobile, dropdown on desktop) by swapping the display: block and display: none properties in the CSS."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 282,
"byteStart": 248
},
"features": [
{
"uri": "https://starlight-sidebar-topics.netlify.app/docs/guides/custom-topic-list/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Moreover, you could also create your own display component, which uses the route data from the Starlight Sidebar Topics plugin and renders the topics in a custom way. This is a bit more advanced, but you can find more information about this in the \"Custom Topics List\" documentation."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreidwzxxzoarucqxg3puav4ieqhe3h3jhwrsvnqtxlhjklkfyxocgjy"
},
"size": 623122,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "Learn how to combine a desktop list and mobile dropdown for your Starlight sidebar topics.",
"publishedAt": "2025-05-13T09:57:00.000Z",
"publication": {
"url": "https://desired-state.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreigyqxqvi4ysi6q2rtnuaz6f37bj5ojfgpounuu36q57qvfbnb2fbq"
},
"size": 14046,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Desired State",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 219,
"g": 219,
"r": 219,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 768,
"accentText": {
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"accentBackground": {
"b": 0,
"g": 170,
"r": 222,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 0,
"g": 170,
"r": 222,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 0,
"g": 0,
"r": 0,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Clear, actionable guides on how to build, configure, and ship.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}How to make your Starlight sidebar items look better
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mpvca73jyk2x",
"data": {
"path": "/3mpvca73jyk2x",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpts4vzcqc2e",
"tags": [
"starlight",
"education",
"css"
],
"$type": "site.standard.document",
"title": "How to make your Starlight sidebar items look better",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f31a3-c69e-7777-aa1b-3a2ce55dc613",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 41,
"byteStart": 32
},
"features": [
{
"uri": "https://starlight.astro.build",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Have you ever wondered why your Starlight sidebar doesn't look that appealing? Did you know how important the space between items in your sidebar unconsciously is? The font size, the weight and little colour differences? In this guide, we'll take a look at how you can customize the appearance of your Starlight sidebar with some quick and easy steps."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Prerequisites"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 45,
"byteStart": 19
},
"features": [
{
"uri": "https://starlight.astro.build/getting-started/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 135,
"byteStart": 77
},
"features": [
{
"uri": "https://starlight.astro.build/guides/css-and-tailwind/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "First, you need to set up your Starlight site. Afterward, Starlight offers a guide on customizing styles applied to your Starlight site, which is the feature we'll use in today's post."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 26,
"byteStart": 16
},
"features": [
{
"uri": "https://starlight.astro.build/guides/css-and-tailwind/#custom-css-styles",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 53,
"byteStart": 49
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 85,
"byteStart": 81
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 176,
"byteStart": 172
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 208,
"byteStart": 199
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 234,
"byteStart": 218
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "As described in this guide, you need to create a .css file somewhere inside your src/ directory, where you can define your CSS styles. Don't forget to add the path to this .css file to Starlightβs customCss array in astro.config.mjs:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "// astro.config.mjs\nimport { defineConfig } from 'astro/config';\nimport starlight from '@astrojs/starlight';\n\nexport default defineConfig({\n integrations: [\n starlight({\n title: 'Docs With Custom CSS',\n customCss: [\n // Relative path to your custom CSS file\n './src/styles/custom.css',\n ],\n }),\n ],\n});",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "After completing these preparation steps, you are ready to try out some nice adjustments to Starlight's sidebar design."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Customizations"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 355,
"byteStart": 354
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#footnote",
"footnoteId": "019f31a8-1123-788c-98ba-7a564b6f13b5",
"contentFacets": [
{
"index": {
"byteEnd": 110,
"byteStart": 94
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"contentPlaintext": "One thing to note though is that in this blog post the focus lies on adjusting the styling of root-level items (the ones without children) in the sidebar."
}
]
}
],
"plaintext": "There are endless different possibilities that you can play around with just with your custom CSS. I will give you some ideas that I found very helpful while playing around with the sidebar design myself. While some of these ideas might look silly to you, I promise that the combination of a few of them will make your Starlight sidebar look even better.*"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "But before I do that, I'll show you how the default styling of the Starlight sidebar currently looks like:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Default styling of the Starlight sidebar",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreifrdpz52mm4islhlxyhjpoqa2iw7gcp3joqeqfzezxhkcbta4fg2y"
},
"size": 19502,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 800,
"height": 340
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Manipulate the whitespaces between sidebar items"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 93,
"byteStart": 88
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 104,
"byteStart": 98
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "On the root level of your Starlight sidebar, there are two different types of elements: pages and groups. While the default styling is pretty decent, I found the whitespaces - this is the margin between items which itself doesn't include any content - to be a bit too large, especially between root-level items. With this example of custom CSS down below, I made the margin between root-level items smaller while keeping the margin between groups the same. The important CSS styling is highlighted in the code block."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "css",
"plaintext": "// src/styles/custom.css\nsl-sidebar-state-persist ul.top-level > li:not(:has(details)) {\n margin-top: 0rem;\n}\nsl-sidebar-state-persist ul.top-level > li:has(details) {\n margin-block: 0.5rem; /* default value */\n}",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Starlight sidebar where the margin between root-level items is smaller",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreifwybdmu5ln4k7zh3wx757ezalhylcmfgqblzm7ebn4i2adabyg24"
},
"size": 19568,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 800,
"height": 340
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Perhaps, this will not be as useful to you because you don't use root-level pages in your sidebar, so this effect won't be noticeable to you. But if you do, give this a try."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Adjust the font weight of sidebar items"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 4,
"byteStart": 0
},
"features": [
{
"uri": "https://en.wiktionary.org/wiki/IMHO",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Imho the one thing that triggers me the most about Starlight's root-level items in the sidebar, is their boldness. This is probably a very opinionated take, but if you ask me, one single page can't possibly be as important as an entire group of pages in your documentation. Therefore, I made the font weight of root-level items thinner as you can see in the code block below."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "css",
"plaintext": "// src/styles/custom.css\nsl-sidebar-state-persist ul.top-level > li > a[aria-current=\"page\"] {\n font-weight: 600; /* default value */\n}\nsl-sidebar-state-persist ul.top-level > li > a:not([aria-current=\"page\"]) {\n font-weight: 400;\n}",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Starlight sidebar with lighter font weight",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreihrrj4hrh24k3iojw6tr74pdmslwdvc7fj5nqruk2gki5yqfnsuzi"
},
"size": 19492,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 800,
"height": 340
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Adjust the colour of sidebar items"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "A small but subtle change: I made unselected root-level items appear dimmer in the code block below."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 310,
"byteStart": 297
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "If you choose to use this design as well, I recommend that you only apply the second CSS manipulation since the first one is just for demonstrating how you could adjust the styling of selected root-level items β this rule also applies to the other code blocks in this blog if they are marked as default value."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "css",
"plaintext": "// src/styles/custom.css\nsl-sidebar-state-persist ul.top-level > li > a[aria-current=\"page\"] {\n color: var(--sl-color-text-invert); /* default value */\n}\nsl-sidebar-state-persist ul.top-level > li > a:not([aria-current=\"page\"]) {\n color: var(--sl-color-gray-2);\n}",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Starlight sidebar with dimmer colors",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreihezoycice3bagc2abmeio64lnq2r2xtulfob3oy6ep4id4cgut3y"
},
"size": 19442,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 800,
"height": 340
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Adjust the font size of sidebar items"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Although I don't recommend it, you can also adjust the font size of sidebar items. With this example of custom CSS down below, I made the font size of root-level items smaller."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "css",
"plaintext": "// src/styles/custom.css\nsl-sidebar-state-persist ul.top-level > li > a {\n font-size: var(--sl-text-sm);\n}",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Starlight sidebar with smaller font size",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreiaj3wckqhhqivp2qvfkhncumqyasuzlepzvrvjh6ivlmi6vcvvywi"
},
"size": 19124,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 800,
"height": 340
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Recommendations"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 142,
"byteStart": 138
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Summing everything up, I recommend that you apply some mix of the above customization options, which you can conveniently see in a single .css file."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 35,
"byteStart": 21
},
"features": [
{
"uri": "https://starlight.astro.build/guides/css-and-tailwind/#cascade-layers",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 69,
"byteStart": 53
},
"features": [
{
"uri": "https://github.com/withastro/starlight/releases/tag/%40astrojs%2Fstarlight%400.34.0",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Note that I also use Cascade Layers, supported since Starlight 0.34.0, and recommend using them to specify the order in which CSS styles are applied."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "css",
"plaintext": "// src/styles/custom.css\n@layer starlight, my-starlight-sidebar;\n\n@layer my-starlight-sidebar {\n sl-sidebar-state-persist ul.top-level > li:not(:has(details)) {\n margin-block: 0rem;\n }\n sl-sidebar-state-persist ul.top-level > li > a:not([aria-current=\"page\"]) {\n font-weight: 400;\n color: var(--sl-color-gray-2);\n }\n}",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Starlight sidebar with recommended CSS adjustments (smaller font, not as bold; dimmed colour)",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreicibv5vcbcm2itqm2ptwx4agpj35nupjycfcpexizm7ifuz34f2ra"
},
"size": 19488,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 800,
"height": 340
}
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreibl3iim6hlp5byaodd2w7wfeexb5kpvwrapqb2lxy5rlz5kp4rqoy"
},
"size": 1213370,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "In this blog post, we'll take a look at how small changes can make a big difference when it comes to whitespaces, font sizes, weights, and colours in your Starlight sidebar.",
"publishedAt": "2025-04-27T09:47:00.000Z",
"publication": {
"url": "https://desired-state.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreigyqxqvi4ysi6q2rtnuaz6f37bj5ojfgpounuu36q57qvfbnb2fbq"
},
"size": 14046,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Desired State",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 219,
"g": 219,
"r": 219,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 768,
"accentText": {
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"accentBackground": {
"b": 0,
"g": 170,
"r": 222,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 0,
"g": 170,
"r": 222,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 0,
"g": 0,
"r": 0,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Clear, actionable guides on how to build, configure, and ship.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Rising community at tomorrow's horizon
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mpv3fqpkgs2i",
"data": {
"path": "/3mpv3fqpkgs2i",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpv2ws6ulc26",
"tags": [
"collaboration",
"npmx"
],
"$type": "site.standard.document",
"title": "Rising community at tomorrow's horizon",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f313b-8891-7cc4-a00a-442bbbd346ac",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "A close-up of a replica of Rodin's \"The Thinker\" statue, depicting a contemplative man resting his chin on his hand.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreieuqekkhwibri2l3hjbui5ruf2f52o4kntswerc4q4a4ikwkjs4ki"
},
"size": 4317848,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 5936,
"height": 3957
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 105,
"byteStart": 95
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "Here I am. Thoughtfully sitting like the French sculptor Auguste Rodin imagined \"The Thinker\" (Le Penseur) in the late 19th century. Thinking about how to write a blog post which should express deepest gratitude, but unable to find the right words to do so."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 533,
"byteStart": 524
},
"features": [
{
"did": "did:plc:u5zp7npt5kpueado77kuihyz",
"$type": "pub.leaflet.richtext.facet#didMention"
}
]
}
],
"plaintext": "Projects come and go. They develop; they move on. Sometimes they finish, but often they don't - especially personal or side projects. But they rarely evolve at light speed. They rarely explode. Nuke the entire ecosystem with their majesty. Rise like a superhuman. Unite. Shine. And inspire. Bring together what was thought far apart. Combining thoughts of hundreds through vision. Making the impossible possible. Sometimes I feel like I should have become a poet. Until I publish my content... and I realize that tools like @npmx.dev are the ink for my digital poetry."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Poetry or not aside: I recently discovered a newly forming community of talented people, connected by the vision of creating a fast and modern browser for the npm registry. What sounds simple on paper - or should I say in the README.md - got features added more quickly than it takes Nutella to sell 1,000 jars of their hazelnut cocoa spread; worldwide β goes without saying. In the early days - before people needed vacation (if you know you know), the young project's main branch struggled to keep up with all the good stuff called \"commits\" in our world that came around every 37 minutes, 24/7. I wouldn't have been amazed if GitHub had spun up a dedicated Azure data centre just for our commitment."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 25,
"byteStart": 21
},
"features": [
{
"uri": "https://npmx.dev/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Now what actually is npmx? For those just discovering it, npmx is a lightning-fast, modern browser for the npm registry, reimagining how developers explore and discover packages. It doesn't host the package itself, it's just a way to interact with all the awesome npm packages you already love. The community plays a very important role, and we define the features we want to have ourselves. The project started with Daniel Roe asking people on Bluesky for what they wished npmjs.org had, without explaining the reason for his question. This way, a handful of useful features for maintainers and consumers like managing, comparing or even liking packages made their way from ideas to reality. It's really impressive what people can build together when they have a common belief, unbounded by possibilities, formed by imagination."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "While many faces were familiar, the sheer number of developers, designers, security and a11y experts, organisers, sponsors and all other professionals that I didn't even know existed before was just mind-blowing. Β‘Ay! Β‘Estoy abrumado! I am definitely proud of what we have already achieved. And I am looking forward to what we will achieve. As developers, but much more as a community. I am here to stay!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Leave a comment or two, depending on how talkative you are π"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "π Resources"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 8,
"byteStart": 0
},
"features": [
{
"uri": "https://npmx.dev",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "npmx.dev - The official npmx website"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 17,
"byteStart": 0
},
"features": [
{
"uri": "https://npmx.dev/blog/alpha-release",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Announcement Post - Read the official alpha blog post"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 13,
"byteStart": 0
},
"features": [
{
"uri": "https://docs.npmx.dev",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Documentation - Get started with npmx"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 14,
"byteStart": 0
},
"features": [
{
"uri": "https://chat.npmx.dev",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Discord Server - Chat with the community"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 8,
"byteStart": 0
},
"features": [
{
"uri": "https://npmx.dev/recharging",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Vacation - One-week break; Discord offline"
}
}
]
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreieuqekkhwibri2l3hjbui5ruf2f52o4kntswerc4q4a4ikwkjs4ki"
},
"size": 4317848,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "This is my way of telling the story of a newly founded community.",
"publishedAt": "2026-03-03T11:00:00.000Z",
"publication": {
"url": "https://still-waters.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreifdtuo4bzzsypm2bvcwmenzr3jueg7nt6afnwcwrbtjnwemi24tri"
},
"size": 12624,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Still Waters",
"$type": "site.standard.publication",
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 43,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 250,
"g": 252,
"r": 253,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Exploring the currents beneath the surface.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}What open source really is about
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mpv3aar7k22n",
"data": {
"path": "/3mpv3aar7k22n",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpv2ws6ulc26",
"tags": [
"mindspace",
"open source"
],
"$type": "site.standard.document",
"title": "What open source really is about",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3139-0c5d-7558-a41f-8237bbafae17",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "A pair of cats napping side by side on a soft bed, creating a serene and cozy scene.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreiazkr3uqtmmqcmwabgjldkyniurjkgfnan5djmse6cwthwrow6zu4"
},
"size": 6255678,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 6000,
"height": 4000
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "tl;dr"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Have fun."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Freedom"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "You could stop reading right now. Contributing to open-source projects is not about creating a reputation. It doesn't bring you any fame. You can't force yourself to create, write or refactor code just for the sake of helping out. Although it might seem that many projects can only be sustainably maintained through more contributors, the additional effort for maintainers to review pull requests that got created due to the need for recognition actually slows down progress as those maintainers have to invest their time into analysing your code. And don't even get me started with vibe-coded issues or code changes."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Experience"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I am not claiming to understand open source and its contributions by any means. And I think everyone has their own opinion on that, which is totally fair because it differs depending on the communities and languages that exist. So in this post, I am gonna share my current personal opinion, which changes over time. It changes in every programmer's lifetime with the experiences you make, socially and in terms of knowledge."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Community"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Contributions are not just coding. And open source is not just about contributions. You can give value by triaging and responding to issues, commenting on discussions, continuously improving workflows, updating dependencies, supporting others with the knowledge you have already learned, or really any non-code contribution as long as you interact with the people in a friendly way. Software is shaped by the people that build it. And you can help build this community around it."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Motivation"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 347,
"byteStart": 340
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "Programming in this sense is the challenge of self-discovery. You have to find what you love. You gotta do what interests you. Your motivation is the only substance that allows you to perform at your best. But you may not know what motivates you from the beginning. Sometimes discipline beats motivation. Some people discover their passion through the discipline of showing up. Not everyone knows what they love from the start."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Journey"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Start simple. Notice a docs typo, share personal feedback in a discussion, report an issue you stumbled upon in your own projects or just get in touch with the people of the community to see if this is the place you would like to be. Nobody is perfect, and neither are most first contributions - even from people who later become maintainers. And you will get a warm welcome even if you make mistakes because that's human. Just avoid over-relying on AI to do the work for you. Learning the codebase yourself is part of the journey. Not every PR will be accepted, and that's okay. Sometimes the timing isn't right, or the approach doesn't fit the project's vision."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Reciprocity"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "If open-source software has helped you in your journey, contributing back helps sustain the ecosystem we all benefit from. When you are working at a company, it makes sense to suggest sponsoring the projects you depend on. Because if maintainers can focus full time on keeping the software up-to-date via financial support, they will not consider changing the FOSS license to a commercial one. If everyone thought this way, everyone would be better off."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Interest"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 126,
"byteStart": 123
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "One thing I noticed during my contributions is that maintainers generally appreciate contributions which actually affected you. This might sound obvious for some of you, but it is a crucial detail my past self also forgot to care about. Fixing issues you didn't encounter yourself can help you learn to recognize common design patterns. They are also useful if you want to get started with a project, on your way to finding your passion with discipline. However, by doing so you give yourself the huge disadvantage of not knowing the motivation behind it."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Enjoyment"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "And I think that this detail sets apart \"good\" from \"bad\" developers. The former ones do not try to be the best, they just mostly do the stuff they enjoy. Open-source projects are created in people's spare time for a reason. I couldn't bear writing software in my free time which doesn't affect me. But I somehow manage to sit down after a full work day, still having the strong will to submit yet another pull request to my favourite open-source project. Because I have fun doing so!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.horizontalRule"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Feel free to share this post with the people you think could benefit from it."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreiazkr3uqtmmqcmwabgjldkyniurjkgfnan5djmse6cwthwrow6zu4"
},
"size": 6255678,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "This is my current opinion regarding making contributions to open source the right way.",
"publishedAt": "2026-02-20T19:05:00.000Z",
"publication": {
"url": "https://still-waters.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreifdtuo4bzzsypm2bvcwmenzr3jueg7nt6afnwcwrbtjnwemi24tri"
},
"size": 12624,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Still Waters",
"$type": "site.standard.publication",
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 43,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 250,
"g": 252,
"r": 253,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Exploring the currents beneath the surface.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Stop Outsourcing Your Soul to AI
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mpv33b5m6k2q",
"data": {
"path": "/3mpv33b5m6k2q",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpv2ws6ulc26",
"tags": [
"mindspace",
"artificial intelligence"
],
"$type": "site.standard.document",
"title": "Stop Outsourcing Your Soul to AI",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f3136-3de8-7336-a792-597d5db7df4f",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "A painting showing a man with a bright, colorful brain, illustrating the concept of cognitive erosion and its impact on thought.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreigi4b5lox3z7egvzgvv6ol5ncxqz3r4ge5zifawvexrgljrzy5kjm"
},
"size": 74576,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 1024,
"height": 585
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "It has been more than two and a half years since the release of ChatGPT. The 30th November, 2022 marked the beginning of a new era. The start of AI. Nowadays, many professions and people in private lives, especially in the information technology sector, use it on a daily basis. And we do not even know where we are on the Gartner hype cycle or if AI will improve following Kurzweilβs \"Law of Accelerating Returns\"."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The trend is still rising, but at the same time I have noticed a slight but steady degradation of knowledge on a human basis because people delegate more and more creative work to AI. As a result, I have decided to write a little blog post, where I can share my thoughts, trying to discourage the overuse of Artificial Intelligence which would ruin our cognitive capabilities. And although I know that very little people read this, it does not stop me from trying. Otherwise, I might have missed my opportunity in helping humanities future."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"facets": [
{
"index": {
"byteEnd": 10,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "Disclaimer: This whole blog post is created from my observations of developments of social changes with special focus on ways of using Artificial Intelligence with my interpretation and is therefore very opinionated. I have not looked at any studies that would support or oppose my opinion."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "How people tend to use AI"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 563,
"byteStart": 537
},
"features": [
{
"uri": "https://www.youtube.com/watch?v=LPZh9BOjkQs",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "AI is awesome. I do not want to question this statement. It helped and still helps me improve my workflow, gathering new knowledge, diving into new waters and exploring new technologies. For people who are not interested in how AI works, such intelligent tools seem like magic. As a result, I like to see their eyes sparkle and their mouths left open when I explain to them that everything AI is based on is probability. What word is most likely to be the next one based on properties and emotions in a highly dimensional virtual space. But that is another topic."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "What I would like to focus on is how people have come to use AI, especially over the past months - if not years. Since our brains are lazy by default, we tend to outsource more work to AI the less focused we are on a specific task. In other words, if you do not put most of your commitment into your current project, you are likely on the verge of just prompting an AI with it. Another potential reason for offshoring work to the AI world is ignorance of the underlying technologies, which makes it difficult for you to come up with your own solutions."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I am by no means a scientist, but I think that watching AI doing your work - because due to your incompetence the outcome you produce is not aligning with your vision - gives you so much dopamine that your brain wants to do it more often. Moreover, the human being as a social figure always aims to be connected to somebody and engaging with AI satisfies those needs. This behaviour is typical for our brains and also happens when watching social media or consuming explicit content. It is deeply marked in our cerebral cortex and the whole gaming industry and entertainment sectors use it recklessly to optimise for money."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I noticed this trend myself - using AI more and more. But I think this is not a shame. Every content creator who uses AI should experience first-hand the pitfalls of creating content directly for the viewer or reader, in order to realise how little they subsequently know about the topic. This will enable them to learn from their mistake and be aware of how easily laziness can lead you down such a despised path. Only the excellent among us strove for hand-crafted content all along."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Negative side effects"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I pointed out the main drawbacks of using AI too extensively that I observed. I will now give you two smaller reasons I have noticed in myself for why you should not use AI for expressive work."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Communication decline"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 111,
"byteStart": 89
},
"features": [
{
"uri": "#resources-with-better-evidence",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "While watching a promotion video from \"The Browser Companyβ’\" (see the last link in the resource section below) I noticed that visions can be very far apart nowadays. I see this trend not only in the Dia browser, but in many different aspects of technology and parts of our lives. One side of the trend wants to integrate AI everywhere possible, widening our dependence on it. One example is using AI to compose and write messages to our closest friends and colleagues because it makes us more efficient. I believe that it just makes us stupid in the long run."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 274,
"byteStart": 262
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "If people do not train their brains for coming up with words for communication, building useful sentences and meaning that can be understood by other human beings, our very foundation of interacting with each other is broken and we would be back at the level of Homo habilis - the last species that likely did not use fully developed verbal language. See, I used AI to help me research this very bit of information, but chose my words consciously to express myself. This way, I not only learned something new, but can now also remember it. A capability you will never have when outsourcing writing to AI."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Loneliness"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Being alone on a mental level is in my opinion one of the most underrated modern dangers of society. The ironic part about it is, that if everyone is alone, we all have it in common, making us not alone with each one's loneliness. But my thoughts are wandering in other oceans again."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 452,
"byteStart": 447
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
]
}
],
"plaintext": "AI strongly encourages us to be alone. It acts was intentionally trained to act as a perfect friend, colleague or whatever you wanna call it. Recently I noticed that ChatGPT's responses explicitly include my name which feels very weird on multiple layers if you ask me. One could argue that this is private artificial doxxing, which of course is a oxymoron in itself as doxxing by definition is public. Nevertheless, the more you use AI, the more often you want to use AI. Particularly after the introduction of AI voice modes this trend kept rising like there is no tomorrow. Congratulations ClosedAI, you just reinvented addiction."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "In worst case scenarios, this can leads people to fear talking with other people. Abandoning friendships. Withdrawal from society. Such habits are serious problems and should best be examined by a doctor or psychologist."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Proposal of AI usage layer model"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Proposing a global solution for all people of such a problem is impossible. Since each individual has their own experiences with AI, their own thoughts and feelings, optimists or pessimists, I cannot speak for everyone. In fact, I think I can only speak for myself, learn and share my own ways of handling and limiting my usage of AI and hope that those experiences can help you as well."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 198,
"byteStart": 195
},
"features": [
{
"uri": "https://en.wikipedia.org/wiki/If_and_only_if",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "My ideal experiences with AI - the optimal golden way between the overuse of AI alongside cognitive erosion and letting the hype pass besides you without taking any advantage of it - is using AI iff you and only you gain any benefits. In other words, control your usage and limit it to an extreme, where only ever you should come into contact with the content the AI produces. Such a limitation has many consequences, here are some examples to better demonstrate and emphasise what I mean:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Use AI for personal learning sessions: If you initially want to dive into a new, completely unknown topic, AI can give you general overviews and show you connections to other parts of knowledge and real-world uses. While this does not necessarily mean that you really learn faster or more efficiently, it helps to take the step forward and get the ball rolling."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Use AI for stupid and repetitive workloads and ask for help if you are certain that the problem or task has already been solved thousands of times by others. Current LLMs are trained based on existing data which means they are meant to be good at solving solved problems, not reinventing the wheel, but suggesting to use the existing round instrument."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Do not use AI to express yourself to other people. Think about what meaning you want to convey, what knowledge you want to share and pack your thoughts into your own words. This helps you immensely to harden your own thoughts and remember what you learned and feel much better."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Do not use AI to generate complex and creative work. Nowadays, there are hundreds - probably thousands - of tools, which help you create art, music, code, designs, plans, etc. with the help of or completely with AI. While this makes it seem that you can be more productive, it just makes you more ignorant the more often you fall back to those methods instead of doing what you love to do yourself. It is completely fine to say that small packages of the work can be outsourced to AI, mostly repetitive tasks that just take more time doing manually. Creative and original work, however, should remain human and made with love. If you do not like what you do at all, this is a signal that you should probably stop doing it at all."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 68,
"byteStart": 61
},
"features": [
{
"uri": "https://lovable.dev",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 84,
"byteStart": 70
},
"features": [
{
"uri": "https://github.com/features/copilot",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 92,
"byteStart": 86
},
"features": [
{
"uri": "https://cursor.com/en",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 107,
"byteStart": 94
},
"features": [
{
"uri": "https://openai.com/index/chatgpt-can-now-see-hear-and-speak/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 135,
"byteStart": 130
},
"features": [
{
"uri": "https://academy.openai.com/public/resources/intro-gpt-5",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Do not fall for all the shiny AI stuff out there. Tools like Lovable, GitHub Copilot, Cursor, ChatGPT Voice and new on the board: GPT-5, just to name a few, do not want to actually help you. They want either directly your data, or own your workflow so you cannot escape once you got used to it. Unfortunately, people get used to AI fascinatingly fast."
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "In short, what I mean is that AI should be used for brainstorming and helping each individual for creating an end product - not to be confused with AI creating the end product -, but not for communicating with other people and the final results themselves (e.g., image creation, writing blogs, posts on social media, communicating with chats, motivation speeches, essays, etc.). In my opinion, the human brain can only evolve and learn from its own mistakes and experience. And you can never learn from your mistakes when it was actually the AI generating them."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"plaintext": "AI has a place in ideation, but expressive work should remain human."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "The Paradigm"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I recently discovered a software design pattern called the \"Onion Architecture\". To keep the explanation short, the main key principle of this architecture is its division into layers, where each layer is only connected to its two neighbours (siblings excluded), the inner and outer layer."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "To transfer this paradigm into the current context, you can imagine the AI as a tool available to you, which is unconnected to all other parts by being only surrounded by your brain, and only your brain can access, manipulate, create, and modify the outer layers - in this example, writing blogs, creating content, communicating with people via chats or speeches, or expressing yourself with art or design."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Here you can see an image that represents my idea of a limited, but ideal usage of AI:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"alt": "Cognitive Erosion Paradigm: Layered, green circles with \"AI\" in the centre, surrounded by the \"Brain\", which is surrounded by multiple circular segments labelled with \"Design\", \"Blog\", \"Chats\", \"Art\", \"Content\", and \"Speech\" clockwise.",
"$type": "pub.leaflet.blocks.image",
"image": {
"ref": {
"$link": "bafkreifsu7zcdzyonn2jwuwcjwfdubto4yyltoqee23ctsdsjcnsr25w4a"
},
"size": 22692,
"$type": "blob",
"mimeType": "image/webp"
},
"aspectRatio": {
"width": 512,
"height": 512
}
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "If such an architectural approach and the paradigm in this context are optimal or just opinionated can be criticised in the comments below."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Conclusion"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "As I said in the beginning, this blog post is very opinionated and aims to just share my experience with everyone so you can also learn from my mistakes and optimise your own usage of AI and have it better under your control. In the end you can do whatever you want, but be aware of the degradation of your own brain when you also fall into the pitfall of overuse."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Also one thing to note: It took me several months to write this blog post, refine it and start from zero again. No blog is perfect as you might have noticed with this one. There are probably some grammatical error here and there, some sentences that actually do not make sense, but this makes humans human. Moreover, there are very likely many strong opinions in this blog where you would totally argue against and completely disagree with me (I'm eager to see your comments expressing your outrage.), however this makes me me. My brain, my thoughts, my opinion."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.blockquote",
"plaintext": "\"errare humanum est\""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Resources with better evidence"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "If you are interested in articles which are based on more scientific studies or approaches, feel free to read some or all of those. They are in no particular order, so just start in the middle and keep going upward or downward. π"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 27,
"byteStart": 0
},
"features": [
{
"uri": "https://lou.gg/blog/ai-artificial-incompetence",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "AI: Artificial Incompetence"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 81,
"byteStart": 0
},
"features": [
{
"uri": "https://www.theregister.com/AMP/2025/07/11/ai_code_tools_slow_down/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "AI coding tools make developers slower but they think they're faster, study finds"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 103,
"byteStart": 0
},
"features": [
{
"uri": "https://futurism.com/companies-fixing-ai-replacement-mistakes",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Companies That Tried to Save Money With AI Are Now Spending a Fortune Hiring People to Fix Its Mistakes"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 64,
"byteStart": 0
},
"features": [
{
"uri": "https://www.tomshardware.com/tech-industry/artificial-intelligence/linus-torvalds-reckons-ai-is-90-percent-marketing-and-10-percent-reality",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Linus Torvalds reckons AI is β90% marketing and 10% realityβ"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 48,
"byteStart": 0
},
"features": [
{
"uri": "https://www.youtube.com/watch?v=Ftt5KqJ5D0Q",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "The AI Girlfriend situation is SAD (YouTube.com)"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 24,
"byteStart": 0
},
"features": [
{
"uri": "https://www.economist.com/science-and-technology/2025/07/16/will-ai-make-you-stupid",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Will AI make you stupid?"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 80,
"byteStart": 0
},
"features": [
{
"uri": "https://x.com/itsalexvacca/status/1935343874421178762",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "AI isn't making us more productive. It's making us cognitively bankrupt. (X.com)"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 31,
"byteStart": 0
},
"features": [
{
"uri": "https://www.forbes.com/sites/lanceeliot/2024/08/24/being-addicted-to-generative-ai/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Being Addicted To Generative AI"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 49,
"byteStart": 0
},
"features": [
{
"uri": "https://tech.co/news/another-study-ai-making-us-dumb",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Yet Another Study Finds That AI Is Making Us Dumb"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 84,
"byteStart": 0
},
"features": [
{
"uri": "https://cognitiontoday.com/is-ai-making-us-smarter-or-dumber-or-artificially-smarter/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Is AI making us smarter Or dumber?β¦ Or artificially smarter? (AI symbiotic crisis)"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 66,
"byteStart": 0
},
"features": [
{
"uri": "https://hbr.org/2025/05/research-gen-ai-makes-people-more-productive-and-less-motivated",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Research: Gen AI Makes People More Productiveβand Less Motivated"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 66,
"byteStart": 0
},
"features": [
{
"uri": "https://www.youtube.com/watch?v=JCZUIm4S9QQ",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "The most powerful ways to \"hack\" our new Dia browser (YouTube.com)"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Happy reading!"
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreigi4b5lox3z7egvzgvv6ol5ncxqz3r4ge5zifawvexrgljrzy5kjm"
},
"size": 74576,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "Reflections on AI overuse, its cognitive and social downsides, and a proposed model for using AI without eroding human creativity and communication.",
"publishedAt": "2025-08-10T07:38:00.000Z",
"publication": {
"url": "https://still-waters.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreifdtuo4bzzsypm2bvcwmenzr3jueg7nt6afnwcwrbtjnwemi24tri"
},
"size": 12624,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Still Waters",
"$type": "site.standard.publication",
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 43,
"g": 130,
"r": 87,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 250,
"g": 252,
"r": 253,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 39,
"g": 39,
"r": 39,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 255,
"g": 255,
"r": 255,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Exploring the currents beneath the surface.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Setting up Continuous deployment in a GitHub repository
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mptxljhmn22z",
"data": {
"path": "/3mptxljhmn22z",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpts4vzcqc2e",
"tags": [
"automation",
"deployment",
"github",
"education"
],
"$type": "site.standard.document",
"title": "Setting up Continuous deployment in a GitHub repository",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f2eeb-a025-7ffe-800f-b583787c9de6",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 91,
"byteStart": 85
},
"features": [
{
"uri": "https://github.com/github",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "In today's post, we'll take a quick look at how to set up continuous deployment in a GitHub repository. We're pretty sure that this setup also works for other Git registries, but if you use another, keep in mind that this post is designed for GitHub only."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 42,
"byteStart": 36
},
"features": [
{
"uri": "https://github.com/github",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 159,
"byteStart": 143
},
"features": [
{
"uri": "/blog/tags/deployment",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "This post also assumes that you use GitHub Actions combined with Argo CD for deploying your applications on a Kubernetes cluster. Follow other deployment posts of us for further instructions on how to set up both technologies on your personal server."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Preparations"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 34,
"byteStart": 24
},
"features": [
{
"uri": "https://hub.docker.com/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "We recommend creating a Docker Hub account or choose another Docker registry if you want."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Your GitHub repository must fulfil these conditions:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Has a Dockerfile (ideally in the root folder)"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 44,
"byteStart": 24
},
"features": [
{
"uri": "https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Has two GitHub Secrets (create GitHub Secret):"
},
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "DOCKER_USERNAME: Your docker username"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 54,
"byteStart": 42
},
"features": [
{
"uri": "https://docs.docker.com/security/for-developers/access-tokens/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "DOCKER_PASSWORD: Your docker password (or access token)"
}
}
]
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Create workflow file(s)"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 114,
"byteStart": 110
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 155,
"byteStart": 138
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 346,
"byteStart": 342
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 509,
"byteStart": 494
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "GitHub Actions are special jobs in GitHub which mostly run on Linux servers and can be controlled by creating yaml files in the directory .github/workflows. These special files can controll after which events these jobs should run and they give you a lot of freedom. As a regular GitHub Action user I can tell you, get used to rewriting your yaml files pretty often because you often forget to think about the small details. But without further ado, let's jump straight into creating a fitting deployment.yaml file, which will do some jobs for us:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Push a new docker image to Docker Hub (with the newest version)."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 35,
"byteStart": 11
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Update the manifest/deployment.yaml file, so Argo CD gets notified about the new tagged image."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "(optional) Create a new release on GitHub, so the times of releases are documented where they should be."
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "# deployment.yaml\nname: Deployment\non:\n push:\n branches: [main]\n merge_group:\n pull_request:\n branches: [main]\n # Allows you to run this workflow manually from the Actions tab\n workflow_dispatch:\n\n# Automatically cancel in-progress actions on the same branch\nconcurrency:\n group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }}\n cancel-in-progress: true\n\nenv:\n REGISTRY: docker.io\n IMAGE_NAME: trueberryless/blog\n NODE_VERSION: 20\n\njobs:\n deployment:\n if: contains(github.event.head_commit.message, 'deploy') || github.event_name == 'workflow_dispatch'\n runs-on: ubuntu-latest\n permissions:\n contents: write\n steps:\n - name: Check out the repo\n uses: actions/checkout@v4\n with:\n fetch-depth: 0\n\n - name: Create tag\n run: echo \"IMAGE_TAG=$(echo $GITHUB_REF_NAME-$GITHUB_SHA)\" >> $GITHUB_ENV\n\n - name: Set up Docker Buildx\n uses: docker/setup-buildx-action@v2\n\n - name: Log in to Docker Hub\n uses: docker/login-action@v2\n with:\n username: ${{ secrets.DOCKER_USERNAME }}\n password: ${{ secrets.DOCKER_PASSWORD }}\n\n - name: Extract metadata (tags, labels) for Docker\n id: meta\n uses: docker/metadata-action@v4\n with:\n images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n - name: Build and push Docker image\n uses: docker/build-push-action@v5\n with:\n context: .\n push: true\n tags: |\n ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}\n ${{ env.IMAGE_NAME }}:latest\n labels: ${{ steps.meta.outputs.labels }}\n\n - name: Update deployment.yaml file\n run: |\n yq eval '.spec.template.spec.containers[0].image = \"${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}\"' -i manifest/deployment.yaml\n\n - uses: stefanzweifel/git-auto-commit-action@v4\n with:\n commit_message: update deployment.json container image (automated)\n\n - uses: ncipollo/release-action@v1\n with:\n tag: ${{ env.IMAGE_TAG }}\n makeLatest: true\n body: \"A docker image has been deployed to [Docker Hub](https://hub.docker.com/r/${{ env.IMAGE_NAME }}/tags).\"",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 49,
"byteStart": 34
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 105,
"byteStart": 88
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 161,
"byteStart": 143
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "After copying the contents of our deployment.yaml file and creating the new file in the .github/workflows folder, you need to do a bit of very important adapting:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 21,
"byteStart": 11
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Change the IMAGE_NAME to your personal Docker Hub repository. The image name consists of your account name and the repository name. If your not sure what your image name is, you can take a look at the URL of the Docker Hub repository, it should be in there somewhere."
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 54,
"byteStart": 48
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Now you should be good to go to add the keyword deploy into any commit message to the main branch of your repository and it should automatically push a docker image to Docker Hub and update the manifest for Argo CD."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Celebrate with a Coffee!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 71,
"byteStart": 65
},
"features": [
{
"uri": "https://github.com/cilium",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 216,
"byteStart": 211
},
"features": [
{
"uri": "https://ko-fi.com/trueberryless",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Congratulations, you've successfully set up Argo CD with k3s and Cilium! You deserve a coffee break. Enjoy a well-earned cup, and if you'd like to share a virtual coffee with me, feel free to support my work on Ko-fi. Thank you!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Appendix"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 41,
"byteStart": 26
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Here you have an obsolete docker-hub.yaml which we used to use because it has nice versioning strategies:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "# docker-hub.yaml\nname: Docker Image Push\non:\n push:\n branches: [main]\n merge_group:\n pull_request:\n branches: [main]\n # Allows you to run this workflow manually from the Actions tab\n workflow_dispatch:\n\n# Automatically cancel in-progress actions on the same branch\nconcurrency:\n group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }}\n cancel-in-progress: true\n\nenv:\n REGISTRY: docker.io\n IMAGE_NAME: trueberryless/blog\n NODE_VERSION: 18\n\njobs:\n docker-push-image:\n if: contains(github.event.head_commit.message, 'version') || github.event_name == 'workflow_dispatch'\n runs-on: ubuntu-latest\n permissions:\n contents: write\n steps:\n - name: Check out the repo\n uses: actions/checkout@v4\n with:\n fetch-depth: 0\n\n - name: Check if file exists\n run: |\n if [ -f .github/artifacts/version.json ]; then\n echo \"File exists\"\n echo \"FILE_EXISTS=true\" >> $GITHUB_ENV\n else\n echo \"File does not exist\"\n echo \"FILE_EXISTS=false\" >> $GITHUB_ENV\n fi\n\n - name: read_json\n if: ${{ env.FILE_EXISTS == 'true' }}\n id: version\n uses: zoexx/github-action-json-file-properties@release\n with:\n file_path: \".github/artifacts/version.json\"\n\n - name: save environment variables\n if: ${{ env.FILE_EXISTS == 'true' }}\n run: |\n echo \"MAJOR=${{steps.version.outputs.major}}\" >> $GITHUB_ENV\n echo \"MINOR=${{steps.version.outputs.minor}}\" >> $GITHUB_ENV\n echo \"PATCH=${{steps.version.outputs.patch}}\" >> $GITHUB_ENV\n\n - name: create environment variables\n if: ${{ env.FILE_EXISTS == 'false' }}\n run: |\n echo \"MAJOR=0\" >> $GITHUB_ENV\n echo \"MINOR=0\" >> $GITHUB_ENV\n echo \"PATCH=0\" >> $GITHUB_ENV\n\n - name: echo environment variables\n run: |\n echo ${{ env.MINOR }}\n echo ${{ env.MINOR }}\n echo ${{ env.MINOR }}\n\n - name: Major version\n if: contains(github.event.head_commit.message, 'major')\n run: |\n echo \"New major version\"\n echo \"MAJOR=$((${{ env.MAJOR }}+1))\" >> $GITHUB_ENV\n echo \"MINOR=0\" >> $GITHUB_ENV\n echo \"PATCH=0\" >> $GITHUB_ENV\n\n - name: Minor version\n if: contains(github.event.head_commit.message, 'minor')\n run: |\n echo \"New minor version\"\n echo \"MINOR=$((${{ env.MINOR }}+1))\" >> $GITHUB_ENV\n echo \"PATCH=0\" >> $GITHUB_ENV\n\n - name: Patch version\n if: contains(github.event.head_commit.message, 'patch') || github.event_name == 'workflow_dispatch'\n run: |\n echo \"New patch version\"\n echo \"PATCH=$((${{ env.PATCH }}+1))\" >> $GITHUB_ENV\n\n - name: Set up Docker Buildx\n uses: docker/setup-buildx-action@v2\n\n - name: Log in to Docker Hub\n uses: docker/login-action@v2\n with:\n username: ${{ secrets.DOCKER_USERNAME }}\n password: ${{ secrets.DOCKER_PASSWORD }}\n\n - name: Extract metadata (tags, labels) for Docker\n id: meta\n uses: docker/metadata-action@v4\n with:\n images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n - name: Build and push Docker image\n uses: docker/build-push-action@v5\n with:\n context: .\n push: true\n tags: |\n ${{ env.IMAGE_NAME }}:${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.PATCH }}\n ${{ env.IMAGE_NAME }}:latest\n labels: ${{ steps.meta.outputs.labels }}\n\n - name: Check out the repo\n uses: actions/checkout@v4\n with:\n fetch-depth: 0\n\n - name: Create folder if necessary\n if: ${{ env.FILE_EXISTS == 'false' }}\n run: mkdir -p .github/artifacts\n\n - name: write_json\n id: create-json\n uses: jsdaniell/create-json@v1.2.2\n with:\n name: \"version.json\"\n json: '{ \"major\": ${{ env.MAJOR }}, \"minor\": ${{ env.MINOR }}, \"patch\": ${{ env.PATCH }} }'\n dir: \".github/artifacts/\"\n\n - uses: stefanzweifel/git-auto-commit-action@v4\n with:\n commit_message: update version.json (automated)\n\n - name: Update deployment.yaml file\n run: |\n yq eval '.spec.template.spec.containers[0].image = \"${{ env.IMAGE_NAME }}:${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.PATCH }}\"' -i manifest/deployment.yaml\n\n - uses: stefanzweifel/git-auto-commit-action@v4\n with:\n commit_message: update deployment.json container image (automated)",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreibz2mnptifzcdi6oedk53ly3lztexmzlu54jogrjt5hpl6z3rmv44"
},
"size": 675880,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "Today we'll take a look at how to set up a GitHub repository which will be deployed to a k3s cluster via Argo CD.",
"publishedAt": "2024-07-28T18:11:00.000Z",
"publication": {
"url": "https://desired-state.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreigyqxqvi4ysi6q2rtnuaz6f37bj5ojfgpounuu36q57qvfbnb2fbq"
},
"size": 14046,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Desired State",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 219,
"g": 219,
"r": 219,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 768,
"accentText": {
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"accentBackground": {
"b": 0,
"g": 170,
"r": 222,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 0,
"g": 170,
"r": 222,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 0,
"g": 0,
"r": 0,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Clear, actionable guides on how to build, configure, and ship.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Setting up Argo CD in a k3s cluster
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mptx4b262k2v",
"data": {
"path": "/3mptx4b262k2v",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpts4vzcqc2e",
"tags": [
"automation",
"deployment",
"education"
],
"$type": "site.standard.document",
"title": "Setting up Argo CD in a k3s cluster",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f2ee5-ab66-7994-994d-d7ff41633e0a",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 68,
"byteStart": 41
},
"features": [
{
"uri": "https://blog.stonegarden.dev/articles/2023/09/argocd-kustomize-with-helm/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 205,
"byteStart": 190
},
"features": [
{
"uri": "https://argo-cd.readthedocs.io/en/stable/getting_started/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 279,
"byteStart": 272
},
"features": [
{
"uri": "https://github.com/argoproj",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 286,
"byteStart": 283
},
"features": [
{
"uri": "https://github.com/k3s-io",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 297,
"byteStart": 291
},
"features": [
{
"uri": "https://github.com/cilium",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 371,
"byteStart": 330
},
"features": [
{
"uri": "/blog/setup-kubernetes-with-cilium-and-cloudflare#setup-certificate-manager-with-cloudflare",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 450,
"byteStart": 402
},
"features": [
{
"uri": "/blog/setup-kubernetes-with-cilium-and-cloudflare",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 500,
"byteStart": 496
},
"features": [
{
"uri": "https://github.com/keel-hq",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 638,
"byteStart": 632
},
"features": [
{
"uri": "https://github.com/github",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Having read Vegard S. Hagen's article βArgo CD Kustomize with Helmβ and decided that their solution was not the way to go for our cluster, we jumped straight into the default Argo CD βGetting startedβ guide. And now we will walk you through the pain of setting up Argo CD on k3s and Cilium, continuing from the chapter βSetup Certificate Manager with Cloudflareβ of our most recent post βSetting up Kubernetes with Cilium and Cloudflareβ. In this exact recent post we also set up Keel at the end, but this step will now be unnecessary because we'll use Argo CD for getting the newest state-of-the-art code from each GitHub repo. Enjoy reading!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 47,
"byteStart": 28
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mptwf35vlc2j",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "We assume that you followed our other blog post."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Requirements"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 57,
"byteStart": 50
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Before we can start, we need to make sure we have kubectl installed, a kubeconfig file (k3s saves this file here:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "#/etc/rancher/k3s/config.yaml\nflannel-backend: \"none\"\ndisable-kube-proxy: true\ndisable-network-policy: true\ncluster-init: true\ndisable:\n - servicelb\n - traefik",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": ") and CoreDNS (check if you have CoreDNS by running this"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "kubectl get pods -n kube-system -l k8s-app=kube-dns",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "command)."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Installation"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "First, let's apply all the necessary services, deployments and many other different kubernetes resources by running:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "kubectl create namespace argocd\nkubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Certificate"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Additionally, we'll need a certificate:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "apiVersion: cert-manager.io/v1\nkind: Certificate\nmetadata:\n name: argocd\n namespace: argocd\nspec:\n secretName: argocd\n issuerRef:\n name: acme-issuer\n kind: ClusterIssuer\n dnsNames:\n - \"argo-cd.trueberryless.org\"",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 64,
"byteStart": 31
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Apply this resource by running kubectl apply -f certificate.yaml."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Ingress Controller"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "And we'll need a ingress controller which is managed by Cilium:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n name: argocd-ingress\n namespace: argocd\nspec:\n rules:\n - host: argo-cd.trueberryless.org\n http:\n paths:\n - path: /\n pathType: Prefix\n backend:\n service:\n name: argocd-server\n port:\n number: 80\n\n tls:\n - hosts:\n - argo-cd.trueberryless.org\n secretName: argocd",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 67,
"byteStart": 31
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Apply this resource by running kubectl apply -f argocd-ingress.yaml."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Deactivate TLS within Argo CD"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 146,
"byteStart": 139
},
"features": [
{
"uri": "https://github.com/argoproj",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 293,
"byteStart": 278
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "With the certificate the connection between the client and server is secured. However, there is still a self-signed certificate within the Argo CD services, which we don't necessarily need. We can therefore deactivate the security of the Argo CD server, by editing the property server.insecure."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "In order to do that, we first run this command:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "kubectl edit cm argocd-cmd-params-cm -n argocd",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "which will hopefully open a file in vim or neovim (otherwise would be cringe if you ask us, LMAO). The file should like something like this:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "# Please edit the object below. Lines beginning with a '#' will be ignored,\n# and an empty file will abort the edit. If an error occurs while saving this file will be\n# reopened with the relevant failures.\n#\napiVersion: v1\ndata:\n server.insecure: \"true\"\nkind: ConfigMap\nmetadata:\n annotations:\n kubectl.kubernetes.io/last-applied-configuration: |\n {\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"labels\":{\"app.kubernetes.io/name\":\"argocd-cmd-params-cm\",\"app.kubernetes.io/part-of\":\"argocd\"},\"name\":\"arg\n creationTimestamp: \"2024-07-27T11:15:28Z\"\n labels:\n app.kubernetes.io/name: argocd-cmd-params-cm\n app.kubernetes.io/part-of: argocd\n name: argocd-cmd-params-cm\n namespace: argocd\n resourceVersion: \"239710156\"\n uid: 5f53d26b-3adf-4ed9-9807-c3da847335a2\ndata:\n server.insecure: \"true\"",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 178,
"byteStart": 175
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 186,
"byteStart": 183
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "The last two lines will probably be not there at first, but this is exactly the setting we want to achieve. Go ahead and add those two lines (marked above) and save the file (Esc β :wq if you're cool)."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Restart the Argo CD Server by running and waiting till the rollout is completed:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "kubectl rollout restart deploy argocd-server -n argocd\n\nkubectl rollout status deploy argocd-server -n argocd",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 87,
"byteStart": 54
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://argo-cd.trueberryless.org",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "After all those steps, we should now see the UI under https://argo-cd.trueberryless.org (password protected)."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 88,
"byteStart": 83
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "The credentials of Argo CD UI consist of a user and a password. The user is always admin and you can get your password by running:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath=\"{.data.password}\" | base64 -d",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Add manifest to repository"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 51,
"byteStart": 44
},
"features": [
{
"uri": "https://github.com/argoproj",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Now in order to create a new application in Argo CD (either UI or CLI β we use UI cause we didn't setup CLI), we need to prepare the Git repository. Because the repository is the single source of truth, this is also the place, where we define all the kubernetes resources which should be created by Argo CD."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 87,
"byteStart": 79
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "We recommend creating a new folder in the git repository called something like manifest. In this folder, we'll create a few files:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 16,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "certificate.yaml:"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 15,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "deployment.yaml:"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 12,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "service.yaml:"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 12,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "ingress.yaml:"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 74,
"byteStart": 64
},
"features": [
{
"uri": "/blog/setup-kubernetes-with-cilium-and-cloudflare#example-app-mutanuq",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 179,
"byteStart": 173
},
"features": [
{
"uri": "https://github.com/github",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 287,
"byteStart": 278
},
"features": [
{
"uri": "/blog/setup-continuous-integration-github-repository",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "These files are basically the exact same files mentioned in the other post but separated into four files because this gives us the advantage to manipulate the manifest from GitHub Actions. But first things first, you'll see how to set up the manifest with GitHub actions in the next post."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Create new application in Argo CD UI"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 35,
"byteStart": 28
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 63,
"byteStart": 56
},
"features": [
{
"uri": "https://github.com/argoproj",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "You'll probably see the big NEW APP button in the UI of Argo CD. Click it and create a new application with adapted properties from below:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 25,
"byteStart": 18
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Application Name: mutanuq"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 21,
"byteStart": 14
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Project Name: default"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 39,
"byteStart": 30
},
"features": [
{
"uri": "/blog/setup-continuous-integration-github-repository",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 54,
"byteStart": 48
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Sync Policy: Find out more in this post / leave Manual for now"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 60,
"byteStart": 16
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Repository URL: https://github.com/trueberryless-org/mutanuq"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 14,
"byteStart": 10
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Revision: HEAD"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 14,
"byteStart": 6
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Path: manifest"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 43,
"byteStart": 13
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Cluster URL: https://kubernetes.default.svc"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 18,
"byteStart": 11
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Namespace: mutanuq"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 44,
"byteStart": 27
},
"features": [
{
"uri": "https://argo-cd.readthedocs.io/en/stable/cli_installation/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Optionally β if you have the CLI installed β you can run this command for the same output:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "argocd app create mutanuq \\\n --project default \\\n --repo https://github.com/trueberryless-org/mutanuq \\\n --revision HEAD \\\n --path manifest \\\n --dest-server https://kubernetes.default.svc \\\n --dest-namespace mutanuq",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Now you can hopefully see your website being deployed in the UI. This process can take some time because for example the certificate request needs to be approved. A healthy application should look something like this:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Celebrate with a Coffee!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 51,
"byteStart": 44
},
"features": [
{
"uri": "https://github.com/argoproj",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 60,
"byteStart": 57
},
"features": [
{
"uri": "https://github.com/k3s-io",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 213,
"byteStart": 208
},
"features": [
{
"uri": "https://ko-fi.com/trueberryless",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Congratulations, you've successfully set up Argo CD on a k3s cluster! You deserve a coffee break. Enjoy a well-earned cup, and if you'd like to share a virtual coffee with me, feel free to support my work on Ko-fi. Thank you!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Continuation"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 30,
"byteStart": 21
},
"features": [
{
"uri": "https://desired-state.leaflet.pub/3mptxljhmn22z",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Be continued for our next blog which will describe how to set up a GitHub repository which can then be deployed via Argo CD."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreignumytemkrr5mfaqqsh5xvbmvpb5p5wbmbxs2mi3wlwchitodwjq"
},
"size": 924064,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "Today we'll take a look at how to set up Argo CD in a k3s cluster.",
"publishedAt": "2024-07-27T20:55:00.000Z",
"publication": {
"url": "https://desired-state.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreigyqxqvi4ysi6q2rtnuaz6f37bj5ojfgpounuu36q57qvfbnb2fbq"
},
"size": 14046,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Desired State",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 219,
"g": 219,
"r": 219,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 768,
"accentText": {
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"accentBackground": {
"b": 0,
"g": 170,
"r": 222,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 0,
"g": 170,
"r": 222,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 0,
"g": 0,
"r": 0,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Clear, actionable guides on how to build, configure, and ship.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Setting up Kubernetes with Cilium and Cloudflare
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mptwf35vlc2j",
"data": {
"path": "/3mptwf35vlc2j",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpts4vzcqc2e",
"tags": [
"deployment",
"education"
],
"$type": "site.standard.document",
"title": "Setting up Kubernetes with Cilium and Cloudflare",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f2ecb-6774-788a-a54f-9b8029136af0",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 19,
"byteStart": 13
},
"features": [
{
"uri": "https://github.com/docker",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 128,
"byteStart": 118
},
"features": [
{
"uri": "https://github.com/kubernetes",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 377,
"byteStart": 373
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 467,
"byteStart": 454
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Working with Docker Containers can be hard. However, there are tools which enhance the management of containers, like Kubernetes. Actually, Kubernetes is the only tool to my knowledge which acts as a management software for Docker Containers. Kubernetes is well-integrated in almost all cloud providers, like Google Cloud, Azure and AWS. As a result, it has a standardized yaml-syntax, which is great for small developers because they can switch between The Big Three with low effort."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "tl;dr"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Install everything and then apply cert-manager. ez"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "curl -sfL https://get.k3s.io | sh -s - \\\n --flannel-backend=none \\\n --disable-kube-proxy \\\n --disable servicelb \\\n --disable-network-policy \\\n --disable traefik \\\n --cluster-init\n\ncurl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash\n\nhelm repo add cilium https://helm.cilium.io/\nhelm repo update\nhelm install cilium cilium/cilium\n\nCILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)\nCLI_ARCH=amd64\ncurl -L --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-${CLI_ARCH}.tar.gz\nsudo tar xzvfC cilium-linux-${CLI_ARCH}.tar.gz /usr/local/bin\nrm cilium-linux-${CLI_ARCH}.tar.gz\n\ncilium install \\\n --set k8sServiceHost=${API_SERVER_IP} \\\n --set k8sServicePort=6443 \\\n --set kubeProxyReplacement=true\n\ncilium status --wait\n\nhelm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set crds.enabled=true",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "# secret-cloudflare.yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: cloudflare-api-key-secret\n namespace: cert-manager\ntype: Opaque\nstringData:\n api-key: <Cloudflare API Token (not encrypted)>\n---\n# cert-issuer.yaml\napiVersion: cert-manager.io/v1\nkind: ClusterIssuer\nmetadata:\n name: acme-issuer\n namespace: cert-manager\nspec:\n acme:\n email: <Email for updates>\n server: https://acme-v02.api.letsencrypt.org/directory\n privateKeySecretRef:\n name: acme-issuer\n solvers:\n - dns01:\n cloudflare:\n email: <Cloudflare account Email>\n apiTokenSecretRef:\n name: cloudflare-api-token-secret\n key: api-token",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Install k3s"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 32,
"byteStart": 21
},
"features": [
{
"uri": "https://blog.stonegarden.dev/articles/2024/02/bootstrapping-k3s-with-cilium/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 56,
"byteStart": 53
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "As Hagen explains in his article, we want to install k3s with no configurations and everything disabled. He describes what components are not installed in details."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "curl -sfL https://get.k3s.io | sh -s - \\\n --flannel-backend=none \\\n --disable-kube-proxy \\\n --disable servicelb \\\n --disable-network-policy \\\n --disable traefik \\\n --cluster-init",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 120,
"byteStart": 103
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 131,
"byteStart": 124
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 252,
"byteStart": 230
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 283,
"byteStart": 277
},
"features": [
{
"uri": "https://github.com/cilium",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "After the installation, there should be some pods running (3). Don't be shocked if the pods are in the ContainerCreating or Pending state. This is because the pods can't communicate between each other because we disabled the CNI (--flannel-backend=none). We will later install Cilium, which will be the replacement of the Flannel CNI."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "kubectl get pods -A",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Install Helm"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 42,
"byteStart": 32
},
"features": [
{
"uri": "https://github.com/kubernetes",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 106,
"byteStart": 97
},
"features": [
{
"uri": "https://helm.sh/docs/intro/install/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Helm is the package manager for Kubernetes, so you should either install it directly (follow the Helm docs) or use parts of Helm which are shipped with Cilium. We chose to install Helm directly, which is easily possible with this command:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "bash",
"plaintext": "curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Install Cilium"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 6,
"byteStart": 0
},
"features": [
{
"uri": "https://github.com/cilium",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Cilium is a networking and security software for Kubernetes. Cilium is very fast, scalable and secure because it's built upon eBPF -- a revolutionary technology that can run sandboxed programs in the Linux kernel without recompiling the kernel or loading kernel modules."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "We could install Cilium with Helm like shown here:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "helm repo add cilium https://helm.cilium.io/\nhelm repo update\nhelm install cilium cilium/cilium",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "However, we wanted to install with their CLI and this is how you can do it. Firstly, install the Cilium CLI by running this code snipped:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)\nCLI_ARCH=amd64\ncurl -L --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-${CLI_ARCH}.tar.gz\nsudo tar xzvfC cilium-linux-${CLI_ARCH}.tar.gz /usr/local/bin\nrm cilium-linux-${CLI_ARCH}.tar.gz",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Then you can install Cilium with your Server IP-Address:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "cilium install \\\n --set k8sServiceHost=${API_SERVER_IP} \\\n --set k8sServicePort=6443 \\\n --set kubeProxyReplacement=true",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 47,
"byteStart": 45
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 59,
"byteStart": 51
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Now we wait until Cilium says, everything is OK or disabled:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "cilium status --wait",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 41,
"byteStart": 34
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "After a while, all pods should be Running."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "kubectl get pods -A",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Last but not least, you can apply some resources for Cilium:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "# announce.yaml\napiVersion: cilium.io/v2alpha1\nkind: CiliumL2AnnouncementPolicy\nmetadata:\n name: default-l2-announcement-policy\n namespace: kube-system\nspec:\n externalIPs: true\n loadBalancerIPs: true",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "# ip-pool.yaml\napiVersion: \"cilium.io/v2alpha1\"\nkind: CiliumLoadBalancerIPPool\nmetadata:\n name: \"first-pool\"\nspec:\n blocks:\n - start: \"192.168.0.240\"\n stop: \"192.168.0.249\"",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 279,
"byteStart": 272
},
"features": [
{
"uri": "https://github.com/Grafana",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Additionally you should upgrade the cilium config. In order to do that with the proper values, first create this file in the root directory where you wanna manage the k3s cluster. Later you could also apply some hubble and prometheus related properties if you want to use Grafana or so (open the collapsed lines if you want to use our config as well)."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "#cilium-config.yaml\nk8sServiceHost: 127.0.0.1\nk8sServicePort: 6443\n\nkubeProxyReplacement: true\nl2announcements:\n enabled: true\n\nexternalIPs:\n enabled: true\n\nk8sClientRateLimit:\n qps: 50\n burst: 200\n\noperator:\n replicas: 1\n rollOutPods: true\n prometheus:\n enabled: true\n\nrollOutCiliumPods: true\n\ningressController:\n enabled: true\n default: true\n loadbalancerMode: shared\n service:\n annotations:\n io.cilium/lb-ipam-ips: 192.168.0.240\n\nhubble:\n relay:\n enabled: true\n ui:\n enabled: true\n metrics:\n serviceMonitor:\n enabled: true\n enableOpenMetrics: true\n enabled:\n - dns\n - drop\n - tcp\n - icmp\n - port-distribution\n - \"flow:sourceContext=workload-name|reserved-identity;destinationContext=workload-name|reserved-identity\"\n - \"kafka:labelsContext=source_namespace,source_workload,destination_namespace,destination_workload,traffic_direction;sourceContext=workload-name|reserved-identity;destinationContext=workload-name|reserved-identity\"\n - \"httpV2:exemplars=true;labelsContext=source_ip,source_namespace,source_workload,destination_ip,destination_namespace,destination_workload,traffic_direction;sourceContext=workload-name|reserved-identity;destinationContext=workload-name|reserved-identity\"\n dashboards:\n enabled: true\n namespace: monitoring\n annotations:\n grafana_folder: \"Hubble\"\n\nprometheus:\n enabled: true\n serviceMonitor:\n enabled: true",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Run this command to upgrade:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "cilium upgrade -f cilium-config.yaml",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Setup Certificate Manager with Cloudflare"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 199,
"byteStart": 189
},
"features": [
{
"uri": "https://github.com/cloudflare",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 351,
"byteStart": 334
},
"features": [
{
"uri": "https://cert-manager.io/docs/configuration/acme/dns01/cloudflare/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "In order to be able to create certificates for each subdomain, it is important to apply a certificate issuer which handles certificate requests and resolves them at some provider. We chose Cloudflare as our issuer and here is the setup which you need to apply to your Kubernetes cluster. For further information you can check out the cert-manager docs."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "But first, we need to install the cert-manager by running the following command:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set crds.enabled=true",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "# cert-issuer.yaml\napiVersion: cert-manager.io/v1\nkind: ClusterIssuer\nmetadata:\n name: acme-issuer\n namespace: cert-manager\nspec:\n acme:\n email: <Email for updates>\n server: https://acme-v02.api.letsencrypt.org/directory\n privateKeySecretRef:\n name: acme-issuer\n solvers:\n - dns01:\n cloudflare:\n email: <Cloudflare account Email>\n apiTokenSecretRef:\n name: cloudflare-api-token-secret\n key: api-token",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 38,
"byteStart": 28
},
"features": [
{
"uri": "https://github.com/kubernetes",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "You can apply a file to the Kubernetes cluster, by running this k8s (also k3s) command:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "kubectl apply -f cluster-issuer.yaml",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "If you want to delete the resource in the Kubernetes cluster, the command is pretty straight forward:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "kubectl delete -f cluster-issuer.yaml",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 203,
"byteStart": 194
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "As you may have spotted above, we also need a secret for the API token which authenticates that this issuer is allowed to request certificates. Therefore, we create a secret with an unencrypted API Token from Cloudflare."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 54,
"byteStart": 44
},
"features": [
{
"uri": "https://github.com/cloudflare",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 122,
"byteStart": 112
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Nowadays we create a token by going to your Cloudflare dashboard, then click on your profile and select the tab API Tokens. Here you can generate a specific token for your issuer or use the Global API Key (not recommended any more). The recommended solution is to create a API token with two permissions (custom token):"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Zone - DNS - Edit"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Zone - Zone - Read"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": ""
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 81,
"byteStart": 66
},
"features": [
{
"uri": "https://developers.cloudflare.com/fundamentals/api/get-started/create-token/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "A more detailed description about the tokens, can be found in the Cloudflare docs."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "After applying this secret to Kubernetes, the issuer should be ready to resolve some bad boys!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "# secret-cloudflare.yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: cloudflare-api-key-secret\n namespace: cert-manager\ntype: Opaque\nstringData:\n api-key: <Cloudflare API Token (not encrypted)>",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "You can now use this issuer by applying this file which will hopefully create a certificate:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "# mutanuq-certificat.yaml\napiVersion: cert-manager.io/v1\nkind: Certificate\nmetadata:\n name: mutanuq\n namespace: mutanuq\nspec:\n secretName: mutanuq\n issuerRef:\n name: acme-issuer\n kind: ClusterIssuer\n dnsNames:\n - \"mutanuq.trueberryless.org\"",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "It usually takes around 90 seconds to authenticate the request once applied. You can check the current status of the request by running this kubernetes command."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 70,
"byteStart": 54
},
"features": [
{
"uri": "#no-cloudflare-certificate-approval",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "If it takes longer than 2 minutes, maybe some tips in #Troubleshooting can help you."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "kubectl describe certificaterequests.cert-manager.io -n mutanuq",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 6,
"byteStart": 4
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "The -n option stands for namespace."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [
{
"index": {
"byteEnd": 19,
"byteStart": 12
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://mutanuq.trueberryless.org",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Example app mutanuq"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Then you can use this certificate in your Ingress controller:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "# mutanuq.yaml\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n name: mutanuq\n namespace: mutanuq\nspec:\n rules:\n - host: mutanuq.trueberryless.org\n http:\n paths:\n - path: /\n pathType: Prefix\n backend:\n service:\n name: mutanuq\n port:\n number: 80\n tls:\n - hosts:\n - mutanuq.trueberryless.org\n secretName: mutanuq",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Setup Keel"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 199,
"byteStart": 193
},
"features": [
{
"uri": "https://github.com/github",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "We always wanted a clean Continuous Integration (CI) and Continuous Delivery (CD) solution for our websites. This means, that a specific commit message should trigger an automated process over GitHub, Docker Hub and our server, which in the end updates the corresponding website after about two minutes."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Keel is a robust software tool which enables this feature for Kubernetes. We used Keel for pulling new Docker Images from Docker Hub by polling every few minutes. Moreover, Keel provides a beautiful dashboard where you can control the polling as well."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "In order to set up Keel with the admin dashboard, we created those files:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 21,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "secret-dashboard.yaml for the Admin Username and Password (not everyone should be able to access the dashboard)"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 9,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 68,
"byteStart": 62
},
"features": [
{
"uri": "https://github.com/keel-hq/keel/blob/9f0a7160bbdc3a107ad148933a4269f30e4e479c/deployment/deployment-template.yaml",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "keel.yaml for the actual k3s configs (copied and adapted from KeelHQ)"
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "# secret-dashboard.yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: keel-dashboard-secrets\n namespace: keel\ntype: Opaque\nstringData:\n username: <username>\n password: <password>",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "# keel.yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: keel\n namespace: keel\n labels:\n app: keel\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: keel\n template:\n metadata:\n labels:\n app: keel\n spec:\n serviceAccountName: keel\n containers:\n - name: keel\n # Note that we use appVersion to get images tag.\n image: \"keelhq/keel:latest\"\n imagePullPolicy: Always\n command: [\"/bin/keel\"]\n env:\n - name: NAMESPACE\n valueFrom:\n fieldRef:\n fieldPath: metadata.namespace\n # Basic auth (to enable UI/API)\n - name: BASIC_AUTH_USER\n valueFrom:\n secretKeyRef:\n name: keel-dashboard-secrets\n key: username\n - name: BASIC_AUTH_PASSWORD\n valueFrom:\n secretKeyRef:\n name: keel-dashboard-secrets\n key: password\n ports:\n - containerPort: 9300\n livenessProbe:\n httpGet:\n path: /healthz\n port: 9300\n initialDelaySeconds: 30\n timeoutSeconds: 10\n resources:\n limits:\n cpu: 100m\n memory: 128Mi\n requests:\n cpu: 50m\n memory: 64Mi",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 92,
"byteStart": 70
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 167,
"byteStart": 157
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "After applying both files and managing the additional certificate for keel.trueberryless.org, the Keel dashboard works perfectly. Moreover, every Kubernetes Deployment can opt in for automated Docker Hub Polling by setting some annotations:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: mutanuq\n namespace: mutanuq\n labels:\n app: mutanuq\n annotations:\n keel.sh/policy: all\n keel.sh/trigger: poll\n keel.sh/pollSchedule: \"@every 1m\"\n keel.sh/releaseNotes: \"https://github.com/trueberryless-org/mutanuq/releases\"\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: mutanuq\n template:\n metadata:\n labels:\n app: mutanuq\n spec:\n containers:\n - name: mutanuq\n image: trueberryless/mutanuq\n imagePullPolicy: Always",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Celebrate with a Coffee!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 54,
"byteStart": 44
},
"features": [
{
"uri": "https://github.com/kubernetes",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 66,
"byteStart": 60
},
"features": [
{
"uri": "https://github.com/cilium",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 81,
"byteStart": 71
},
"features": [
{
"uri": "https://github.com/cloudflare",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 226,
"byteStart": 221
},
"features": [
{
"uri": "https://ko-fi.com/trueberryless",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Congratulations, you've successfully set up Kubernetes with Cilium and Cloudflare! You deserve a coffee break. Enjoy a well-earned cup, and if you'd like to share a virtual coffee with me, feel free to support my work on Ko-fi. Thank you!"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Troubleshooting"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Cilium-ingress has no External-IP"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 26,
"byteStart": 19
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 95,
"byteStart": 84
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Make sure that the ip-pool includes the address specified by the annotations in the config.yaml file."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "# ip-pool.yaml\napiVersion: \"cilium.io/v2alpha1\"\nkind: CiliumLoadBalancerIPPool\nmetadata:\n name: \"first-pool\"\nspec:\n blocks:\n - start: \"192.168.0.240\" # 240 included for ingress\n stop: \"192.168.0.249\"",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "#cilium-config.yaml\ningressController:\n enabled: true\n default: true\n loadbalancerMode: shared\n service:\n annotations:\n io.cilium/lb-ipam-ips: 192.168.0.240 # this must be within range",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Also in some cases, other ingress controllers get the annotated address before the Cilium IC can access it, so it would still be pending..."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 55,
"byteStart": 42
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "If you don't deploy locally but on one of The Big Three, please check out some other documentation on why the External IP is still pending. It's mostly their obligation to provide you with an address."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "No Cloudflare Certificate Approval"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "There can be some problem when the certificate won't get approved by Cloudflare."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 4,
"plaintext": "Wrong API token"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "First make sure that the Cloudflare API token is correct. To make 100 percent sure, create a new one and put it (not base64 encoded) into this file:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "apiVersion: v1\nkind: Secret\nmetadata:\n name: cloudflare-api-key-secret\n namespace: cert-manager\ntype: Opaque\nstringData:\n api-key: <Cloudflare API Token (not encrypted)>",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 4,
"plaintext": "Max auth failures reached"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 106,
"byteStart": 27
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "We once ran into the error Error: 9109: Max auth failures reached, please check your Authorization header.. Just wait a few hours, delete the resource and apply it again:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "kubectl delete -f certificate.yaml\nkubectl apply -f certificate.yaml",
"syntaxHighlightingTheme": "catppuccin-mocha"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Hopefully, you're now good to go!"
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreifriepxwelftkzutlpeoaqlvh42wix7vb5mhafmajqjljkqqmgx5e"
},
"size": 274770,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "Today we'll take a look at how to set up a Kubernetes cluster with K3s and Cilium.",
"publishedAt": "2024-06-11T16:43:00.000Z",
"publication": {
"url": "https://desired-state.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreigyqxqvi4ysi6q2rtnuaz6f37bj5ojfgpounuu36q57qvfbnb2fbq"
},
"size": 14046,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Desired State",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 219,
"g": 219,
"r": 219,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 768,
"accentText": {
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"accentBackground": {
"b": 0,
"g": 170,
"r": 222,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 0,
"g": 170,
"r": 222,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 0,
"g": 0,
"r": 0,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Clear, actionable guides on how to build, configure, and ship.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}Designing a Kafka-Based Pipeline and Interactive Graph for Energy Anomaly Detection
{
"id": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.document/3mptsiqpfzs2o",
"data": {
"path": "/3mptsiqpfzs2o",
"site": "at://did:plc:pbjvqaziagcyv2vqodldn5op/site.standard.publication/3mpts4vzcqc2e",
"tags": [
"tool",
"collaboration"
],
"$type": "site.standard.document",
"title": "Designing a Kafka-Based Pipeline and Interactive Graph for Energy Anomaly Detection",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019f2e9d-8723-755c-ae6e-c9bcedd8adab",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "During my fifth year at HTL Krems (2023/24), every student was required to complete a diploma thesisβa substantial project split into two main parts: theoretical and practical. Working in pairs, we had to produce a theoretical paper of around 100 pages, where we introduced, analysed, and explained our chosen topic."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The theoretical part was organized into three sections. The first involved gathering and summarizing existing knowledge from credible sources like books and previous theses. The second focused on the insights and findings derived from our prototypeβa software solution we developed for the practical component. Finally, the third section combined these two, offering a critical comparison and reflection on what we learned through the project as a whole."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The practical aspect required us to design and implement a solution, either software or hardware-based, to tackle a real-world problem. My teammate, Clemens Schlipfinger, and I decided to focus exclusively on software to ensure reliability. Hardware can introduce unpredictable issues, but softwareβespecially when deployed in robust Docker containersβoffered a consistent environment that worked seamlessly everywhere."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "What made our project particularly exciting was our partnership with Siemens AG, a renowned multinational company specializing in energy, automation, and digitalization. This collaboration was rare among our peers and added a professional dimension to our work. Siemens tasked us with developing a service-oriented software solution for analysing anomalies in Europe's power grid."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The system we built comprised several components, each designed to handle a specific function. At the core was a Kafka application responsible for ingesting dataβpower grid anomaly reports we termed Findings. These reports detailed failures and irregularities across Europe's intricate power network, comprising millions of interconnected components like transistors, switches, and generators. Kafka transmitted the most recent anomalies to a PostgreSQL database, which was then made accessible via a GraphQL API. This API provided comprehensive features like pagination, filtering, and querying to ensure seamless integration with our Angular-based frontend."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The frontend was designed as a user-friendly dashboard with three key sections:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.orderedList",
"children": [
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Anomalies Table β A searchable, paginated, and filterable table for quick navigation of anomalies."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Graph View β A dynamic visualisation of anomaly data, making it easier to pinpoint the source of an issue within the vast, complex grid."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Overall Dashboard β Key metrics and insights for quick, actionable overviews."
}
}
],
"startIndex": 1
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Clemens and I divided our responsibilities evenly. Clemens handled the backend systems, including Kafka, PostgreSQL, and the GraphQL API, ensuring a reliable and efficient data pipeline. I took on the role of project leader, coordinating communication with Siemens while developing the frontend and ensuring the seamless integration of the various components. This collaboration allowed us to play to our strengths and maintain a balanced workload."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Our final product not only met Siemens' requirements but also demonstrated the power of a well-designed service-oriented architecture (SOA) in solving complex, real-world problems. It was immensely rewarding to see our software functioning reliably and providing actionable insights to tackle power grid anomalies."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 69,
"byteStart": 65
},
"features": [
{
"uri": "https://trueberryless.org/thesis.pdf",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 104,
"byteStart": 100
},
"features": [
{
"uri": "https://videos.trueberryless.org/videos/thesis/",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "For more details about our work, feel free to explore our thesis here π or read our video course here π₯ (both only available in German)."
}
}
]
}
]
},
"coverImage": {
"ref": {
"$link": "bafkreif3x5wjrcrdfyqayf3i57rnkvt4oxl4r2r5igccjv4r5qwnbpsakm"
},
"size": 821250,
"$type": "blob",
"mimeType": "image/webp"
},
"description": "A SOA solution with Siemens AG to analyze power grid anomalies, featuring a Kafka pipeline, PostgreSQL, GraphQL API, and an Angular dashboard for visualisation.",
"publishedAt": "2024-05-13T10:00:00.000Z",
"publication": {
"url": "https://desired-state.leaflet.pub",
"icon": {
"ref": {
"$link": "bafkreigyqxqvi4ysi6q2rtnuaz6f37bj5ojfgpounuu36q57qvfbnb2fbq"
},
"size": 14046,
"$type": "blob",
"mimeType": "image/webp"
},
"name": "Desired State",
"$type": "site.standard.publication",
"theme": {
"$type": "pub.leaflet.publication#theme",
"primary": {
"b": 219,
"g": 219,
"r": 219,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageWidth": 768,
"accentText": {
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgb"
},
"pageBackground": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"backgroundColor": {
"a": 100,
"b": 24,
"g": 24,
"r": 26,
"$type": "pub.leaflet.theme.color#rgba"
},
"accentBackground": {
"b": 0,
"g": 170,
"r": 222,
"$type": "pub.leaflet.theme.color#rgb"
},
"showPageBackground": false
},
"basicTheme": {
"$type": "site.standard.theme.basic",
"accent": {
"b": 0,
"g": 170,
"r": 222,
"$type": "site.standard.theme.color#rgb"
},
"background": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
},
"foreground": {
"b": 0,
"g": 0,
"r": 0,
"$type": "site.standard.theme.color#rgb"
},
"accentForeground": {
"b": 24,
"g": 24,
"r": 26,
"$type": "site.standard.theme.color#rgb"
}
},
"description": "Clear, actionable guides on how to build, configure, and ship.",
"preferences": {
"showComments": true,
"showMentions": true,
"showPrevNext": true,
"showFirstLast": false,
"showInDiscover": true,
"showRecommends": true
}
}
}
}