diff options
21 files changed, 1002 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75337ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +__pycache__ +/output diff --git a/content/2025-04-28.md b/content/2025-04-28.md new file mode 100644 index 0000000..880fafb --- /dev/null +++ b/content/2025-04-28.md @@ -0,0 +1,524 @@ +Title: Wiki Devblog - April 14 through April 28 +Slug: wd-2025-04-28 +Date: 2025-04-28 +Category: wiki devblog +Summary: <div class="toc"><ul><li><a href="/wd-2025-04-28.html#code-bug-fixes">bug fixes</a><i> • </i></li><li><a href="/wd-2025-04-28.html#design-dated-commentary-displacement">dated commentary displacement</a><i> • </i></li><li><a href="/wd-2025-04-28.html#design-white-space">white space</a><i> • </i></li><li><a href="/wd-2025-04-28.html#feature-artworks-attaching-above">artworks attaching above</a><i> • </i></li><li><a href="/wd-2025-04-28.html#feature-search-filters">search filters</a><i> • </i></li><li><a href="/wd-2025-04-28.html#feature-search-fuzzing">search fuzzing</a><i> • </i></li><li><a href="/wd-2025-04-28.html#data-more-multiple-artworks">more multiple artworks</a><i> • </i></li><li><a href="/wd-2025-04-28.html#code-content-entries">content entries</a><i> • </i></li><li><a href="/wd-2025-04-28.html#coffee">coffee</a></li></ul></div> + +[TOC] + +<section id="code-bug-fixes" markdown="block"> +### bug fixes (<a href="#top">top</a>) {: .code #code-bug-fixes-heading data-toc-label="bug fixes"} + +Right after [release](https://hsmusic.wiki/news/the-apple-wizard-of-mount-fuji/), there were a handful of pretty harsh bugs to get fixed up! + +With the addition of "multiple lyrics", the internal representation for tracks *without* lyrics was made into an empty array, rather than `null`. We overlooked this in the ["Tracks - with Lyrics"](https://hsmusic.wiki/list/tracks/with-lyrics/) listing, which started displaying *every single track!* + +Very strange quirks in the CSS for "wiki edits" tooltips were causing them to [escape the page layout](https://media.hsmusic.wiki/misc/changelog/edits-tooltip-positioning-woes-old.png), now that this element appears by in the cover art column by the right. [We fixed that!](https://media.hsmusic.wiki/misc/changelog/edits-tooltip-positioning-woes-new.png) That involved tidying some nonsense to do with *dynamically* positioning this tooltip in particular (because its natural position was out of bounds). + +We also fixed the cool and new `--sort` option screwing up Windows line endings. This worked out a lot neater than we worried going in. Line endings still scare us though. + +Thanks to Misty for catching the broken lyrics listing, Lilith for spotting out-of-bounds wiki edits, and everyone for keeping cool with `--sort`. +</section> + +<section id="design-dated-commentary-displacement" markdown="block"> + +### dated commentary displacement (<a href="#top">top</a>) {: .design #design-dated-commentary-displacement-heading data-toc-label="dated commentary displacement" } + +From their outset, dated commentary entries have posed a trouble. The non-technical gist is that they need to *fit*, and they're pretty wide... so we just forced them past the cover art column. + +<figure markdown="block"> +<img width="2448" height="1578" + alt="The track Indigo Archer, which is more or less a blank page with just an artwork and some release details; there's a lot of empty padding before the artist commentary section begins." + src="{static}/2025-04-28/dated-commentary-displacement/indigo-archer.png"> +<figcaption>It works, but...</figcaption> +</figure> + +This is sort of alright when you've got just one cover artwork. [It's less nice when you have two.]({static}/2025-04-28/dated-commentary-displacement/multi-artworks.png) + +So we tried a new style. + +<figure markdown="block"> +<img width="2434" height="1718" + alt="Same track page, but the dates for the entries all hang beneath the right edge of the heading. The commentary section no is no longer pushed down - it starts right after the rest of the release info." + src="{static}/2025-04-28/dated-commentary-displacement/hanging.png"> +<figcaption>Snazzy? Maybe?</figcaption> +</figure> + +It wasn't all that popular, though. It just feels cluttered! + +We ended up going way simpler. + +<figure markdown="block"> +<img width="2394" height="1694" + alt="Same track page. The commentary section still starts right away. And the dates look completely normal... they're just sitting right after the annotation, rathre than floating off aligned to the right." + src="{static}/2025-04-28/dated-commentary-displacement/no-nonsense.png"> +<figcaption>It doesn't take a rocket scientist.</figcaption> +</figure> +</section> + +<section id="design-white-space" markdown="block"> +### white space (<a href="#top">top</a>) {: .design #design-white-space-heading data-toc-label="white space" } + +A marked amount of the wiki's design and technical work, in total, go to making sure it's responsive at different screen sizes. We mean *really* responsive; sure, there are three mildly different "layouts" for thin, medium, and wide displays... but the trick is getting *finely* differing layouts to look good. This involves lots of terrifying word-wrapping and inline-block and templating-directive science. + +But simpler changes are sometimes due, too. Due to an infuriatingly naive technical concession, we instated a `max-width: 600px` rule for most content elements very early on. Check out the screenshots in ["dated commentary displacement"](#design-dated-commentary-displacement){.design} - you're surely used to the layout as it is, and that's thanks to this inconspicuous style. + +We got rid of it! + +<figure markdown="block"> +<img width="3424" height="2158" + alt="Ruins Rising, a track page with two artworks, some reference info, and commentary. The commentary entries extend further right than normal." + src="{static}/2025-04-28/white-space/ruins-rising.png"> +<figcaption>Fresh air</figcaption> +</figure> + +With a keen eye, you might tell we've tweaked the dimensions for cover artworks, too. Not counting the thin layout (which makes cover artworks fill most of the screen), the effective range used to be 200 to 300 pixels; we've tightened that by about 20px on both edges. + +The responsiveness math is such that you can comfortably view the art... + +* at its largest size, while the sidebar is visible, yet still with a variety of screen widths +* at its smallest size, while the sidebar is visible +* at its largest size, while the sidebar is collapsed +* at its smallest size, while the sidebar is collapsed (but still wider than the "thin" layout) + +The crux of all this is hopefully that you - the user! - get a lot more control over the space that stuff on the page really takes up, just by stretching or squishing your browser window. And no matter the dimensions - maybe you're on a tablet or in full screen and don't *get* to decide - content should take the space that's available a little more gracefully. +</section> + +<section id="feature-artworks-attaching-above" markdown="block"> +### artworks attaching above (<a href="#top">top</a>) {: .feature #feature-artworks-attaching-above-heading data-toc-label="artworks attaching above" } + +While porting lots of art about the wiki to the new "multiple artworks" system, we quickly realized that a lot of art... closely accompanies the main artwork... and, consequently, ought to be presented differently than standalone pieces! + +Actually, what stood out was that no one really needs the same tag list in their face *twice.* So we addressed that directly... and then did a similar trick for repeated artist information... and realized this was grounds for a proper feature, really. + +So in data, for any artwork, you're now able to specify `Attach Above: true`. This, well, attaches that artwork to the artwork specified above it. + +<figure markdown="block"> +<img width="2774" height="2160" + alt="Three artworks in column for the album Comfortable Bugs. The first is shown bigger; the other two are the same size, and attached with a neat little chain link in the middle. The first two have full details, while the third's info area is briefer." + src="{static}/2025-04-28/artwork-attaching-above/comfortable-bugs.png"> +<figcaption>Comfortable Artworks</figcaption> +</figure> + +For Comfortable Bugs above, the third artwork has `Attach Above: true`. This means... + +* The third artwork attaches to the second one. +* The third artwork, by default, inherits artist contributions and art tags from the second one. +* The third artwork won't display artist info if it has the same artists (and annotations—all in the same order) as the second one. +* The third artwork won't display tag info if it has the same art tags (in the same order) as the second one. +* The second artwork *always* displays its full info, because it's *not* marked `Attach Above: true`. It also doesn't inherit any details from any other artwork. +* The first artwork doesn't affect any of this, because the third artwork attaches to the second one - not the first! + +If there are multiple artworks in a row which have all got `Attach Above: true`, even though they're *displayed* as a multiple-link chain, they really all attach to the first-above artwork which *isn't* marked `Attach Above: true`. If Comfortable Bugs had a fourth artwork (wow) then it would attach to the second one - meaning it inherits its artists and art tags from the second one, not the third, even though the third is literally right above it. + +And that detail matters, because it's perfectly possible for artworks which `Attach Above` to *override* those details that are inherited by default! If we spot [a secret Stutzman](https://hsmusic.wiki/tag/scott-stutzman/) in the background environ of artwork #3 - mysteriously missing from #4 - then his tag will *not* be inherited onto #4. Likewise, if he really is present in both #3 and #4, he's got to be specified for both. + +Attached artworks are a pretty fun feature, but - as of today! - they aren't really integrated outside of info pages, right now. They're really treated just the same as any other secondary artwork. (You could say wiki code opts into being smart with attached artworks... but by default, all the existing code will continue to work - completely ignoring that status.) There's probably plenty room to do more here. As they say - we'll surely see! +</section> + +<section id="feature-search-filters" markdown="block"> +### search filters (<a href="#top">top</a>) {: .feature #feature-search-filters-heading data-toc-label="search filters" } + +We added a search bar to the wiki [back in June 2024](https://hsmusic.wiki/news/snow-pollen-appreciation-station/). Only it's hardly "back in", because the wiki had been around for *four and a half years* prior to that point. Regardless, we're going on one year since, and search has changed exactly not at all. + +Some spark inspired us that perhaps there ought to be a "search by lyrics" feature on the music wiki - just type up some text into the search box like normal and, if it happens to match any lyrics in the background, those would come up as a little "Lyrics" tab, just alongside the rest of the filter buttons! + +Then we realized that those filter buttons still only existed in our head. (Despite having thought at them since before search even released, and having [laid out concrete plans](https://github.com/hsmusic/hsmusic-wiki/issues/589) back in January this year.) So we got them added! + +<figure markdown="block"> +<video controls width="1410" height="1596" + src="{static}/2025-04-28/search-filter/search-filter.mp4"></video> +</figure> + +This one was fun. Sort of like the search bar itself, there isn't a whole lt to say about it. We went for a simpler UI than we imagined at the start - there's no "All" button, the default state is just to to *not* have a filter active. We hope it's pretty clear that you can, well, clear the filter, by just clicking it again - but we'll mess around with the design some more if that turns out not to be the case. + +If you've got a filter active, then it's kept active for your next query too, as long as the results (in total) include some within that filter. Otherwise the filter just gets cleared automatically. + +The old style of filtering results, where you just type out the type of thing as part of the query, like `rj artist`, still works too. It's even integrated nice and fancily here: + +<figure markdown="block"> +<video controls width="1298" height="1596" + src="{static}/2025-04-28/search-filter/search-typed-filter.mp4"></video> +</figure> + +Since the typed filter is, well, typed, the only way to *disable* it is to take it out of your search query. The wiki won't do that for you, so the filter "button" is inert. +</section> + +<section id="feature-search-fuzzing" markdown="block"> +### search fuzzing (<a href="#top">top</a>) {: .feature #feature-search-fuzzing-heading data-toc-label="search fuzzing" } + +Yep, even more search! We posted the main demo for <a href="#feature-search-filters" class="feature">search filters</a> and everyone loved it, except also (in particular - thanks! - Makin) pointed out that fuzzy search kinda matters more. You know, as far as "gotta do SOMETHING before search goes untouched for an entire year" goes. + +Once we'd done a bit of sleuthing around the FlexSearch documentation, we added forward-fuzzy search, which is just about the same as having an asterisk after every word in your search query. + +And if that's coming up with the wrong results, just wrap it in quotes! + +<figure markdown="block"> +<video controls width="1410" height="1596" + src="{static}/2025-04-28/search-fuzzing/verbatim-fuzz.mp4"></video> +</figure> + +We hesitate to liken the quotes thing to "verbatim" search, even though that's what it's internally called and obviously we're using quotes and of course it's verbatim search. But it isn't really. We couldn't figure out how to get FlexSearch to care about the *order* of words. `"land fans music of"` comes up with exactly the same results. + +We're also missing out on some, like... integration... with... the core of how search even works, on the music wiki. It's a whole thing, but the gist is that if your search query happens to include both verbatim and non-verbatim terms, then those are going to run *completely separate searches*, and the final results will just be whichever are common across both. It's still very fast and will probably find you what you're looking for... as long as you're probably just searching by name, and not meaning to match any other fields. + +FlexSearch is indeed very flexible, but if you want to get much of anything done with it, you'll have to get good at flexing it yourself. And we're still learning! +</section> + +<section id="data-more-multiple-artworks" markdown="block"> +### more multiple artworks (<a href="#top">top</a>) {: .data #data-more-multiple-artworks data-toc-label="more multiple artworks" } + +In the data department we mostly extended the new "multiple artworks" feature to cover a bunch more artworks scattered about the official discography. We worked at this from two angles: + +1. Tidy up the `media/misc` folder, putting the most common categories of remaining standalone art into subfolders. We already had a `changelog` folder, for example - now there's `anthology-wips` and `references` too. Then just trudge through the whole dang `misc` folder, top to bottom, A to Z, searching where each image is used and sticking 'em into "multiple artworks" everywhere applicable. + +2. Tackle everything from any given album all at once, so stuff from a similar context is tidied up together. We did Homestuck Vol. 7 this way, for example. That was fun! + +In particular, courtesy of Meulanie, we had an awesome list of *sources* for the artwork for Vol. 7 (plus a few pieces from later releases). Vol. 7 in particular collates most of its track artwork from, well, fanart - pieces that might have been making the rounds towards the end of album production, as far as we can tell. + +Most of Meulanie's sources were booru pages (web galleries where folk repost others' artworks, mostly for the sake of archival and searchability) - so, not the original sources. However, these booru pages in turn pointed to the *actual* source URLs. Those were usually from DeviantArt or Tumblr... and long, long gone, too. Trouble in paradise - it's a shame not to preserve and link back where any artwork really was originally posted. But we knew that Tumblr post URLs still work as long as the posts haven't been deleted - if only you can figure out the latest URL for the blog itself. We spent most of an afternoon digging around the artist info we've got (and the Wayback Machine), and were fortunately able to figure out up-to-date sources for lots of those artworks! + +As you might surmise - because all this fanart was not made with an album in mind, it usually wasn't square. Even when we couldn't locate the original source, the boorus preserved those original cuts! So we've taken all those and worked them onto the wiki as secondary artworks, providing the best source links we were able to find. +</section> + +<section id="code-content-entries" markdown="block"> +### content entries (<a href="#top">top</a>) {: .code #code-content-entries-heading data-toc-label="content entries" } + +In YAML data, `Commentary` fields typically look something like this: + +``` +Commentary: |- + <i>Alex Rosetti:</i> (composer, [Tumblr](https://albatrossthesoup.tumblr.com/post/20545830112/my-vol-5-music-commentary), excerpt, 4/5/2012) + + I composed this one intending it to be the theme of Jade’s land. This is the reason [[artist:erik-scheele|Jit]] has gone on the record as saying [[track:crystamanthequins|Crystamenthequins]] was intended to accompany Jade’s island being destroyed/her entering her land (I’m pretty sure that’s how he described it) was because of this song. It’s actually a really simple tune [...] + + <i>Chumi:</i> (anthology artist, [Tumblr](https://chuchumi.tumblr.com/post/159546207889/heres-my-entry-for-the-volume-5-anthology-i-was), 4/13/2017) + + here’s my entry for the [volume 5 anthology](https://vol5anthology.tumblr.com/)! i was lucky enough to be able to draw for [crystalanthemums](https://www.youtube.com/watch?v=j8GVlwRrXC8)! (do yourself a favor and check out everyone else’s AMAZING art and the video pls…) +``` + +Content code (broadly - the stuff that turns data into pages!) ends up processing a data structure more like this: + +``` +track.commentary: [ + { + artists: [ <Artist "Alex Rosetti"> ] + annotation: "composer, [Tumblr](https://albatrossthesoup ..." + date: Date 4/5/2012 + body: "I composed this one intending it to be ..." + }, + { + artists: [ <Artist "Chumi"> ] + annotation: "anthology artist, [Tumblr](https://chuchumi ..." + date: Date 4/13/2017 + body: "here's my entry for the [volume 5 ..." + } +] +``` + +<details markdown="block"> + <summary>This code section is long as fuck. READ ON, IF YOU DARE. :sparkles:</summary> +There's quite a slab of processing which turns form A into form B. That processing mainly operates a rather beefy bit of regular expression: + +``` +const dateRegex = groupName => + String.raw`(?<${groupName}>[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,4}[-/][0-9]{1,4}[-/][0-9]{1,4})`; + +const commentaryRegexRaw = + String.raw`^<i>(?<artistReferences>.+?)(?:\|(?<artistDisplayText>.+))?:<\/i>(?: \((?<annotation>(?:.*?(?=,|\)[^)]*$))*?)(?:,? ?(?:(?<dateKind>sometime|throughout|around) )?${dateRegex('date')}(?: ?- ?${dateRegex('secondDate')})?(?: (?<accessKind>captured|accessed) ${dateRegex('accessDate')})?)?\))?`; +``` + +This regex defines what a commentary heading looks like. We weren't interested in messing with the wiki's actual data format, so we left it alone, continuing to work with the regex as it was. + +However, the regex on its own doesn't take care of everything! Here's some stuff we take care of external to the regex itself: + +* Matching artist references, like `"Alex Rosetti"`, to their actual `Artist` objects (we represent this as `<Artist "Alex Rosetti">` - it means you can do stuff like `entry.artists[0].name` or `relation('linkArtist', entry.artists[0])`) +* Converting date text into `Date` objects +* Extracting a web.archive.org URL from the annotation, filling in `accessDate` (and `accessKind: 'capture'`) +* Getting the body of the entry - the text *between* each heading, and following the final one! +* Tying everything up into the final object shape + +We used to perform all of those steps in one great big "composition", which is the pipeline-style framework which basically all of the wiki's nitty-gritty data processing is written in. It's very cool and powerful and frankly quite simple; but its greatest superpower is, perhaps, giving us all too unwell a feeling when what we're doing is Too Freaking Complicated. + +<figure markdown="block"> +<img width="1464" height="1805" + alt="Terrifyingly tall code window. Looks like a serial process of some sort..." + src="{static}/2025-04-28/content-entries/composition.png"> +<figcaption>This is stressful.</figcaption> +</figure> + +Compositions are basically meant to work with flat lists of values - usually simple values. They *can* do much more - because you can express just about any wiki data problem in those terms - but it starts to get bulky, quick. + +In particular, *this* composition was responsible for making *the entire* shape that is expected of content entries (the shape we demoed above). This involves, for example, filling in defaults for each entry. + +``` +fillMissingListItems({ + list: '#entries.artistDisplayText', + fill: input.value(null), +}), + +fillMissingListItems({ + list: '#entries.annotation', + fill: input.value(null), +}), +``` + +And extracting the list of artist references for each entry. + +``` +{ + dependencies: ['#entries.artistReferences'], + compute: (continuation, { + ['#entries.artistReferences']: artistReferenceTexts, + }) => continuation({ + ['#entries.artistReferences']: + artistReferenceTexts + .map(text => text.split(',').map(ref => ref.trim())), + }), +}, +``` + +And matching those references with their actual `Artist` objects... wait for it... for each entry. + +``` +withFlattenedList({ + list: '#entries.artistReferences', +}), + +withResolvedReferenceList({ + list: '#flattenedList', + find: inputSoupyFind.input('artist'), + notFoundMode: input.value('null'), +}), + +withUnflattenedList({ + list: '#resolvedReferenceList', +}).outputs({ + '#unflattenedList': '#entries.artists', +}), +``` + +It's all delightfully doable, but boring. And why are we processing... every... single... entry?? During each step of the process? + +That's just how compositions work. They're a straight-shot, "A to Z", process *everything, now!* kind of coding. It's useful for doing lots of closely related tasks in order, where each task clearly builds on previous ones. + +And honestly, it works alright for the stuff which runs the regular expression... + +<figure markdown="block"> +<img width="1032" height="1724" + alt="Processing matches from a regular expression with the same sort of composition coding. Funky, but it clearly works." + src="{static}/2025-04-28/content-entries/regex-composition.png"> +<figcaption>This is actually almost reasonable, if you squint.</figcaption> +</figure> + +But past the regex, we're handling a bunch of *independent* tasks. The only reason we even bother with all of them is here and now, well, we've got our input shape, and we're obliged to provide our output shape... + +Fortunately we had a minor a-ha moment, very shortly after release. We'd recently worked out a neat and simple way to perform `Thing`-style processing on the level of *sub-*things. We figured, hey... the interface we coded for this... would probably just work for commentary entries, right...??? + +The answer is yeah totally - it did! We swapped the guts of `commentary` properties (lyrics and crediting sources, too) over the course of an afternoon. + +"Thing-style processing" just refers to the framework which makes up all the actual objects - an `Album` is a thing, so is every `Artist` and every `Track`, the likes. (It's kinda OOP-flavored, although that's secondary to just being a useful practice of encapsulation.) + +Well, now every `ContentEntry` is also a thing. And those independent tasks are evaluated... independently! + +Defaults are totally free, because on `Thing` objects, *every* property either has a value (as specified) or is `null`. + +``` +static [Thing.getPropertyDescriptors] = ({Artist}) => ({ + ... + + artistText: contentString(), + annotation: contentString(), + + ... +}); +``` + +And matching references is trivial. That's a first-class operation because it represents like 90% of what the music wiki does LOL: + +``` +... + artists: referenceList({ + class: input.value(Artist), + find: soupyFind.input('artist'), + }), +... +``` + +Alright, but how about *parsing* those references? You know, the `text.split(',').map(ref => ref.trim())` part? And what about the regular expression? How are we *getting* these `ContentEntry` objects? + +The guts are a dead-normal JavaScript function. + +``` +export function matchContentEntries(sourceText) { + const matchEntries = []; + + let previousMatchEntry = null; + let previousEndIndex = null; + + for (const {0: matchText, index: startIndex, groups: matchEntry} + of sourceText.matchAll(commentaryRegexCaseSensitive)) { + if (previousMatchEntry) { + previousMatchEntry.body = sourceText.slice(previousEndIndex, startIndex); + } + + matchEntries.push(matchEntry); + + previousMatchEntry = matchEntry; + previousEndIndex = startIndex + matchText.length; + } + + if (previousMatchEntry) { + previousMatchEntry.body = sourceText.slice(previousEndIndex); + } + + return matchEntries; +} +``` + +In fact, this doesn't even "process" the regular expression so much as just *run* it. The final output, per entry, is literally the `groups` from the raw matches, plus a `body` property filled in with the relevant text. + +The boilerplate is a bit more interesting, but let's look at that from the top down. + +``` +static [Thing.yamlDocumentSpec] = { + fields: { + ... + + 'Commentary': { + property: 'commentary', + transform: parseCommentary, + }, + + ... + }, +}; +``` + +Just a normal `transform` function, for converting some raw YAML value into a JavaScript-lookin' shape. So what's going on in `parseCommentary`? + +``` +export function parseCommentary(sourceText, {subdoc, CommentaryEntry}) { + return parseContentEntries(CommentaryEntry, sourceText, {subdoc}); +} + +export function parseCreditingSources(sourceText, {subdoc, CreditingSourcesEntry}) { + return parseContentEntries(CreditingSourcesEntry, sourceText, {subdoc}); +} + +export function parseLyrics(sourceText, {subdoc, LyricsEntry}) { + if (!multipleLyricsDetectionRegex.test(sourceText)) { + const document = {'Body': sourceText}; + + return [subdoc(LyricsEntry, document, {bindInto: 'thing'})]; + } + + return parseContentEntries(LyricsEntry, sourceText, {subdoc}); +} +``` + +`parseCommentary` is one of several wrapper functions. They're mostly a handy shorthand, though `parseLyrics` has a little more going on (the wiki *optionally* supports multiple "lyrics entries" per track). + +`parseContentEntries` is the real point of conversion, from regex groups to `ContentEntry` documents. And - look! There's the code that splits the artist reference lists! + +``` +export function parseContentEntries(thingClass, sourceText, {subdoc}) { + const map = matchEntry => ({ + 'Artists': + matchEntry.artistReferences + .split(',') + .map(ref => ref.trim()), + + 'Artist Text': + matchEntry.artistDisplayText, + + 'Annotation': + matchEntry.annotation, + + 'Date': + matchEntry.date, + + 'Second Date': + matchEntry.secondDate, + + 'Date Kind': + matchEntry.dateKind, + + 'Access Date': + matchEntry.accessDate, + + 'Access Kind': + matchEntry.accessKind, + + 'Body': + matchEntry.body, + }); + + const documents = + matchContentEntries(sourceText) + .map(matchEntry => + withEntries( + map(matchEntry), + entries => entries + .filter(([key, value]) => + value !== undefined && + value !== null))); + /* editor's note: we literally just weren't + * sure whether unmatched regex groups have + * the value `undefined` or `null` LOL + */ + + const subdocs = + documents.map(document => + subdoc(thingClass, document, {bindInto: 'thing'})); + + return subdocs; +} +``` + +The magic here is in `subdoc`. That's the "sub-things" feature we were referring to earlier, which we figured out for multiple artworks. And it's so simple to use: just give it something that looks like an ordinary YAML document, and it'll turn that into a bona fide `Thing` instance, prepared just the same as any top-level *real* YAML document! (yes subdocs can include subdocs lol) + +The transformation function, `parseContentEntries`, amounts to the real YAML we specified earlier amounting to just this: + +``` +Commentary: +- Artists: + - Alex Rosetti + Annotation: >- + composer, [Tumblr](https://albatrossthesoup.tumblr.com/post/20545830112/my-vol-5-music-commentary), excerpt + Date: 4/5/2012 # This is still a string! + Body: |- + I composed this one intending it to be the theme of Jade’s land. This is the reason [[artist:erik-scheele|Jit]] has gone on the record as saying [[track:crystamanthequins|Crystamenthequins]] was intended to accompany Jade’s island being destroyed/her entering her land (I’m pretty sure that’s how he described it) was because of this song. It’s actually a really simple tune [...] +- Artists: + - Chumi + Annotation: >- + anthology artist, [Tumblr](https://chuchumi.tumblr.com/post/159546207889/heres-my-entry-for-the-volume-5-anthology-i-was) + Date: 4/13/2017 + Body: |- + here’s my entry for the [volume 5 anthology](https://vol5anthology.tumblr.com/)! i was lucky enough to be able to draw for [crystalanthemums](https://www.youtube.com/watch?v=j8GVlwRrXC8)! (do yourself a favor and check out everyone else’s AMAZING art and the video pls…) +``` + +That is a little bit junk to write in real data filalso es. `transform: parseCommentary` lets us stick with the format we're used to; it's just a little boilerplate that gives us access to all the benefits of standalone `ContentEntry` things! +</details> +</section> + +<section id="coffee" markdown="block"> +### ☕️ (<a href="#top">top</a>) {: .coffee #coffee data-toc-label="☕️"} + +Over the last handful of months, we've done a bunch of our really focused wiki work... at a coffee shop!! They are super nice and never kick us out, very cool and sweet on their part. We buy our keep with like one coffee every two and a half hours and it is lovely. + +We also realized that like, it would not be the END of the world if we gave people a way to ✨support our work✨. So we did! We've got a Ko-fi now! [ko-fi.com/qzneb](https://ko-fi.com/qzneb) wowwwww + +Hosting the wiki is pretty cheap and never something we're gonna hold over the head of "donations please", so ko-fi tips DO NOT cover server or domain costs, not one bit. We're still happily handling those out of pocket. Ko-fi tips buy us coffee. Yum yum yum you can now caffeinate us for free for money. Thank you very much! + +**COFFEE COUNT:** LIKE SEVEN OR SO<br> +**YOU** (yes you) **TIPPED US:** three of them I think :) thank you + +If you're reading this when it's supposed to be linked, hopefully all the stuff we've gone over here is live on [the preview website](https://preview.hsmusic.wiki)!! Including our multiple artworks data stuff, even though that's not merged into the official preview branch yet. Check it out check it out, offer feedback [in Discord](https://hsmusic.wiki/discord/) or [over email](https://hsmusic.wiki/feedback/), and see you in probably two weeks!! + +[https://preview.hsmusic.wiki](https://preview.hsmusic.wiki)<br> +[https://ko-fi.com/qzneb](https://ko-fi.com/qzneb)<br> +<a href="https://en.wikipedia.org/wiki/Slime_%28Dragon_Quest%29" class="secret">slurp</a> + +<i>~ QN</i> +</section> diff --git a/content/2025-04-28/artwork-attaching-above/comfortable-bugs.png b/content/2025-04-28/artwork-attaching-above/comfortable-bugs.png new file mode 100644 index 0000000..ea3cf85 --- /dev/null +++ b/content/2025-04-28/artwork-attaching-above/comfortable-bugs.png Binary files differdiff --git a/content/2025-04-28/content-entries/composition.png b/content/2025-04-28/content-entries/composition.png new file mode 100644 index 0000000..bdffbb7 --- /dev/null +++ b/content/2025-04-28/content-entries/composition.png Binary files differdiff --git a/content/2025-04-28/content-entries/regex-composition.png b/content/2025-04-28/content-entries/regex-composition.png new file mode 100644 index 0000000..68d25ec --- /dev/null +++ b/content/2025-04-28/content-entries/regex-composition.png Binary files differdiff --git a/content/2025-04-28/dated-commentary-displacement/hanging.png b/content/2025-04-28/dated-commentary-displacement/hanging.png new file mode 100644 index 0000000..f9a8a21 --- /dev/null +++ b/content/2025-04-28/dated-commentary-displacement/hanging.png Binary files differdiff --git a/content/2025-04-28/dated-commentary-displacement/indigo-archer.png b/content/2025-04-28/dated-commentary-displacement/indigo-archer.png new file mode 100644 index 0000000..58fd6f4 --- /dev/null +++ b/content/2025-04-28/dated-commentary-displacement/indigo-archer.png Binary files differdiff --git a/content/2025-04-28/dated-commentary-displacement/multi-artworks.png b/content/2025-04-28/dated-commentary-displacement/multi-artworks.png new file mode 100644 index 0000000..b06f316 --- /dev/null +++ b/content/2025-04-28/dated-commentary-displacement/multi-artworks.png Binary files differdiff --git a/content/2025-04-28/dated-commentary-displacement/no-nonsense.png b/content/2025-04-28/dated-commentary-displacement/no-nonsense.png new file mode 100644 index 0000000..3fcacb2 --- /dev/null +++ b/content/2025-04-28/dated-commentary-displacement/no-nonsense.png Binary files differdiff --git a/content/2025-04-28/search-filter/search-filter.mp4 b/content/2025-04-28/search-filter/search-filter.mp4 new file mode 100644 index 0000000..ec8b690 --- /dev/null +++ b/content/2025-04-28/search-filter/search-filter.mp4 Binary files differdiff --git a/content/2025-04-28/search-filter/search-typed-filter.mp4 b/content/2025-04-28/search-filter/search-typed-filter.mp4 new file mode 100644 index 0000000..46624be --- /dev/null +++ b/content/2025-04-28/search-filter/search-typed-filter.mp4 Binary files differdiff --git a/content/2025-04-28/search-fuzzing/verbatim-fuzz.mp4 b/content/2025-04-28/search-fuzzing/verbatim-fuzz.mp4 new file mode 100644 index 0000000..90ae00c --- /dev/null +++ b/content/2025-04-28/search-fuzzing/verbatim-fuzz.mp4 Binary files differdiff --git a/content/2025-04-28/white-space/ruins-rising.png b/content/2025-04-28/white-space/ruins-rising.png new file mode 100644 index 0000000..e0f0c4b --- /dev/null +++ b/content/2025-04-28/white-space/ruins-rising.png Binary files differdiff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..adc162e --- /dev/null +++ b/nodemon.json @@ -0,0 +1,7 @@ +{ + "ignore": [ + "./output/**/*", + "./output2/**/*" + ], + "ext": "*" +} diff --git a/pelicanconf.py b/pelicanconf.py new file mode 100644 index 0000000..f780c8a --- /dev/null +++ b/pelicanconf.py @@ -0,0 +1,31 @@ +AUTHOR = 'nebula' +SITENAME = "Nebula's Blog" +SITEURL = "https://nebula.hsmusic.wiki" + +PATH = "content" +THEME = "theme/neb" + +TIMEZONE = 'Canada/Atlantic' +DEFAULT_LANG = 'en' +DEFAULT_PAGINATION = 10 + +RELATIVE_URLS = True + +MARKDOWN = { + "extension_configs": { + "markdown.extensions.attr_list": {}, + + "markdown.extensions.codehilite": { + "guess_lang": False, + }, + + "markdown.extensions.fenced_code": {}, + "markdown.extensions.md_in_html": {}, + "markdown.extensions.meta": {}, + + "markdown.extensions.toc": { + "title": "table of contents", + }, + }, + "output_format": "html5", +} diff --git a/theme/neb/static/css/codehilite-coffee.css b/theme/neb/static/css/codehilite-coffee.css new file mode 100644 index 0000000..1f7de46 --- /dev/null +++ b/theme/neb/static/css/codehilite-coffee.css @@ -0,0 +1,85 @@ +pre { line-height: 125%; } +td.linenos .normal { color: #4e4e4e; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: #4e4e4e; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #8f9494; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #8f9494; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.codehilite .hll { background-color: #ddd0c0 } +.codehilite { background: #262220; color: #ddd0c0 } +.codehilite .c { color: #70757A } /* Comment */ +.codehilite .err { color: #af5f5f } /* Error */ +.codehilite .esc { color: #ddd0c0 } /* Escape */ +.codehilite .g { color: #ddd0c0 } /* Generic */ +.codehilite .k { color: #919191 } /* Keyword */ +.codehilite .l { color: #af875f } /* Literal */ +.codehilite .n { color: #ddd0c0 } /* Name */ +.codehilite .o { color: #878787 } /* Operator */ +.codehilite .x { color: #ddd0c0 } /* Other */ +.codehilite .p { color: #ddd0c0 } /* Punctuation */ +.codehilite .ch { color: #8f9f9f } /* Comment.Hashbang */ +.codehilite .cm { color: #70757A } /* Comment.Multiline */ +.codehilite .cp { color: #fdd0c0 } /* Comment.Preproc */ +.codehilite .cpf { color: #c9b98f } /* Comment.PreprocFile */ +.codehilite .c1 { color: #70757A } /* Comment.Single */ +.codehilite .cs { color: #af5f5f } /* Comment.Special */ +.codehilite .gd { color: #bb6868 } /* Generic.Deleted */ +.codehilite .ge { color: #ddd0c0; font-style: italic } /* Generic.Emph */ +.codehilite .ges { color: #ddd0c0 } /* Generic.EmphStrong */ +.codehilite .gr { color: #af5f5f } /* Generic.Error */ +.codehilite .gh { color: #ddd0c0 } /* Generic.Heading */ +.codehilite .gi { color: #849155 } /* Generic.Inserted */ +.codehilite .go { color: #ddd0c0 } /* Generic.Output */ +.codehilite .gp { color: #ddd0c0 } /* Generic.Prompt */ +.codehilite .gs { color: #ddd0c0; font-weight: bold } /* Generic.Strong */ +.codehilite .gu { color: #ddd0c0 } /* Generic.Subheading */ +.codehilite .gt { color: #af5f5f } /* Generic.Traceback */ +.codehilite .kc { color: #875f5f } /* Keyword.Constant */ +.codehilite .kd { color: #875f5f } /* Keyword.Declaration */ +.codehilite .kn { color: #875f5f } /* Keyword.Namespace */ +.codehilite .kp { color: #919191 } /* Keyword.Pseudo */ +.codehilite .kr { color: #b46276 } /* Keyword.Reserved */ +.codehilite .kt { color: #af875f } /* Keyword.Type */ +.codehilite .ld { color: #af875f } /* Literal.Date */ +.codehilite .m { color: #87afaf } /* Literal.Number */ +.codehilite .s { color: #c9b98f } /* Literal.String */ +.codehilite .na { color: #ddd0c0 } /* Name.Attribute */ +.codehilite .nb { color: #ddd0c0 } /* Name.Builtin */ +.codehilite .nc { color: #875f5f } /* Name.Class */ +.codehilite .no { color: #af8787 } /* Name.Constant */ +.codehilite .nd { color: #fdd0c0 } /* Name.Decorator */ +.codehilite .ni { color: #ddd0c0 } /* Name.Entity */ +.codehilite .ne { color: #877575 } /* Name.Exception */ +.codehilite .nf { color: #fdd0c0 } /* Name.Function */ +.codehilite .nl { color: #ddd0c0 } /* Name.Label */ +.codehilite .nn { color: #ddd0c0 } /* Name.Namespace */ +.codehilite .nx { color: #ddd0c0 } /* Name.Other */ +.codehilite .py { color: #dfaf87 } /* Name.Property */ +.codehilite .nt { color: #87afaf } /* Name.Tag */ +.codehilite .nv { color: #ddd0c0 } /* Name.Variable */ +.codehilite .ow { color: #878787 } /* Operator.Word */ +.codehilite .pm { color: #ddd0c0 } /* Punctuation.Marker */ +.codehilite .w { color: #ddd0c0 } /* Text.Whitespace */ +.codehilite .mb { color: #87afaf } /* Literal.Number.Bin */ +.codehilite .mf { color: #87afaf } /* Literal.Number.Float */ +.codehilite .mh { color: #87afaf } /* Literal.Number.Hex */ +.codehilite .mi { color: #87afaf } /* Literal.Number.Integer */ +.codehilite .mo { color: #87afaf } /* Literal.Number.Oct */ +.codehilite .sa { color: #dfaf87 } /* Literal.String.Affix */ +.codehilite .sb { color: #c9b98f } /* Literal.String.Backtick */ +.codehilite .sc { color: #c9b98f } /* Literal.String.Char */ +.codehilite .dl { color: #c9b98f } /* Literal.String.Delimiter */ +.codehilite .sd { color: #878787 } /* Literal.String.Doc */ +.codehilite .s2 { color: #c9b98f } /* Literal.String.Double */ +.codehilite .se { color: #af5f5f } /* Literal.String.Escape */ +.codehilite .sh { color: #c9b98f } /* Literal.String.Heredoc */ +.codehilite .si { color: #af5f5f } /* Literal.String.Interpol */ +.codehilite .sx { color: #fdd0c0 } /* Literal.String.Other */ +.codehilite .sr { color: #af5f5f } /* Literal.String.Regex */ +.codehilite .s1 { color: #c9b98f } /* Literal.String.Single */ +.codehilite .ss { color: #af5f5f } /* Literal.String.Symbol */ +.codehilite .bp { color: #87afaf } /* Name.Builtin.Pseudo */ +.codehilite .fm { color: #fdd0c0 } /* Name.Function.Magic */ +.codehilite .vc { color: #ddd0c0 } /* Name.Variable.Class */ +.codehilite .vg { color: #ddd0c0 } /* Name.Variable.Global */ +.codehilite .vi { color: #ddd0c0 } /* Name.Variable.Instance */ +.codehilite .vm { color: #ddd0c0 } /* Name.Variable.Magic */ +.codehilite .il { color: #87afaf } /* Literal.Number.Integer.Long */ diff --git a/theme/neb/static/css/neb-anim.css b/theme/neb/static/css/neb-anim.css new file mode 100644 index 0000000..45de2fd --- /dev/null +++ b/theme/neb/static/css/neb-anim.css @@ -0,0 +1,31 @@ +@property --shine1 { + syntax: '<percentage>'; + initial-value: 0%; + inherits: false; +} + +@property --shine2 { + syntax: '<percentage>'; + initial-value: 0%; + inherits: false; +} + +@keyframes label-shine { + from { + --shine1: 0%; + --shine2: 0%; + } + + 20% { + --shine2: 0%; + } + + 80% { + --shine1: 100%; + } + + 100% { + --shine1: 100%; + --shine2: 100%; + } +} diff --git a/theme/neb/static/css/neb-style.css b/theme/neb/static/css/neb-style.css new file mode 100644 index 0000000..daf798a --- /dev/null +++ b/theme/neb/static/css/neb-style.css @@ -0,0 +1,261 @@ +@import url(codehilite-coffee.css); +@import url(neb-anim.css); + +:root { + font-family: sans-serif; + + --color: #0088ff; + + --code-color: #de2891; + --coffee-color: #ee211d; + --data-color: #18d211; + --design-color: #d26b2a; + --feature-color: #7f35ee; + + .code { --color: var(--code-color); } + .coffee { --color: var(--coffee-color); } + .data { --color: var(--data-color); } + .design { --color: var(--design-color); } + .feature { --color: var(--feature-color); } +} + +body { + margin: 25px; + margin-bottom: 60px; + + &::before { + content: ""; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; + + background-color: #c9c9c9; + background-image: url(/theme/img/5002.png); + background-size: 125px; + } + + > header, + > main, + > footer { + max-width: 968px; + margin-left: auto; + margin-right: auto; + + background: white; + } + + > header { + padding: 20px 20px 10px 20px; + + background-color: black; + background-image: url(/theme/img/5003.png); + color: white; + background-size: 125px; + + > nav ul { + display: flex; + padding: 0; + margin-bottom: 10px; + + li { + display: block; + + &:has(+ .right) { + flex-grow: 1; + } + } + + li a { + padding: 5px 20px; + + background: #272727; + border: 1px solid var(--color); + border-radius: 2px; + + &:where(li.active a) { + background: var(--color); + color: white; + } + } + } + } + + > footer { + padding: 5px 20px 5px 20px; + background-color: #f3f3f3; + } + + > main { + --content-padding: 20px; + padding: var(--content-padding); + + contain: paint; + + article > header > :first-child { + margin-top: 0; + } + } +} + +a { + color: var(--color); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + + &.secret { + color: inherit; + } +} + +article { + line-height: 1.6; + + code:not(:where(pre code)) { + padding: 2px 3px; + background-color: color-mix(in srgb, var(--color), 85% #00000003); + border: 1px solid color-mix(in srgb, var(--color), 70% #00000002); + border-radius: 3px; + } + + pre { + line-height: 1.3; + white-space: pre-wrap; + background: #0006; + border: 4px inset #ccce !important; + padding: 12px 8px; + } + + li { + /* baseline, paragraphs will space further */ + margin-bottom: 0.25em; + } + + figure { + img, video { + display: block; + margin-left: auto; + margin-right: auto; + margin-bottom: 1em; + max-width: 100%; + max-height: 80vh; + box-shadow: 0 1px 3px 1px #3336; + width: auto; + height: auto; + } + + figcaption { + text-align: center; + font-style: oblique; + } + } + + .toc { + font-weight: 800; + + li i { display: none; } + + li::before { + --pale: color-mix(in srgb, var(--color), 65% #ffffff); + --shine: color-mix(in srgb, var(--color), 75% white); + + background: var(--pale); + padding: 3px 7px; + margin-right: 7px; + border-bottom-right-radius: 5px; + border-top-left-radius: 5px; + } + + li:has(a:hover)::before { + background: + linear-gradient(45deg, + var(--pale) var(--shine2), + var(--shine) calc(var(--shine2) + 1%), + var(--shine) var(--shine1), + var(--pale) calc(var(--shine1) + 1%)); + + animation: label-shine 0.35s linear forwards; + } + + li:has(a[href*="#code"])::before { + content: "code"; --color: var(--code-color); + } + + li:has(a[href*="#coffee"])::before { + content: "coffee"; --color: var(--coffee-color); + } + + li:has(a[href*="#data"])::before { + content: "data"; --color: var(--data-color); + } + + li:has(a[href*="#design"])::before { + content: "design"; --color: var(--design-color); + } + + li:has(a[href*="#feature"])::before { + content: "feature"; --color: var(--feature-color); + } + } + + h3 { + margin-left: calc(-1 * var(--content-padding)); + margin-right: calc(-1 * var(--content-padding)); + padding-left: calc(0.5 * var(--content-padding)); + padding-right: calc(1.5 * var(--content-padding)); + padding-top: 6px; + padding-bottom: 4px; + margin-top: 1.75em; + scroll-margin-top: 0.75em; + + position: sticky; + top: 0; + background: #fffc; + box-shadow: 0 3px 4px #fff3; + border-bottom: 2px solid #22222238; + backdrop-filter: + contrast(0.9) brightness(1.4) contrast(0.2) brightness(2.0) saturate(1.4) blur(6px); + + &::before { + background: color-mix(in srgb, var(--color), 65% #ffffff); + padding: 5px 10px; + margin-right: 10px; + border-bottom-right-radius: 5px; + border-top-left-radius: 5px; + } + + &.code::before { content: "code"; } + &.coffee::before { content: "coffee"; } + &.data::before { content: "data"; } + &.design::before { content: "design"; } + &.feature::before { content: "feature"; } + } + + h3 { font-size: 1.17em; } + h3 { margin-bottom: 0; } + h3 + * { margin-top: 1.17em; } + + footer { + line-height: 1.2; + p { margin: 0; } + + &::before { + content: ""; + display: block; + width: 14ch; + height: 2px; + margin-bottom: 1em; + margin-top: 3em; + background: #c7c7c7; + } + + address { + font-style: normal; + } + } +} diff --git a/theme/neb/static/img/5002.png b/theme/neb/static/img/5002.png new file mode 100644 index 0000000..7f0bfb8 --- /dev/null +++ b/theme/neb/static/img/5002.png Binary files differdiff --git a/theme/neb/static/img/5003.png b/theme/neb/static/img/5003.png new file mode 100644 index 0000000..b33c3e9 --- /dev/null +++ b/theme/neb/static/img/5003.png Binary files differdiff --git a/theme/neb/templates/base.html b/theme/neb/templates/base.html new file mode 100644 index 0000000..53350ef --- /dev/null +++ b/theme/neb/templates/base.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<html lang="{% block html_lang %}{{ DEFAULT_LANG }}{% endblock html_lang %}"> + <head> + {% block head %} + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <meta name="generator" content="Pelican" /> + <title>{% block title %}{{ SITENAME|striptags }}{%endblock%}</title> + <link rel="stylesheet" type="text/css" href="{{ SITEURL }}/theme/css/neb-style.css" /> + {% if FEED_ALL_ATOM %} + <link href="{{ FEED_DOMAIN }}/{% if FEED_ALL_ATOM_URL %}{{ FEED_ALL_ATOM_URL }}{% else %}{{ FEED_ALL_ATOM }}{% endif %}" type="application/atom+xml" rel="alternate" title="{{ SITENAME|striptags }} Atom Feed" /> + {% endif %} + {% if FEED_ALL_RSS %} + <link href="{{ FEED_DOMAIN }}/{% if FEED_ALL_RSS_URL %}{{ FEED_ALL_RSS_URL }}{% else %}{{ FEED_ALL_RSS }}{% endif %}" type="application/rss+xml" rel="alternate" title="{{ SITENAME|striptags }} RSS Feed" /> + {% endif %} + {% block extra_head %}{% endblock extra_head %} + {% endblock head %} + </head> + + <body id="index" class="home"> + <header id="banner" class="body"> + <h1><a href="{{ SITEURL }}/">{{ SITENAME }}{% if SITESUBTITLE %} <strong>{{ SITESUBTITLE }}</strong>{% endif %}</a></h1> + <nav><ul> + {% for title, link in MENUITEMS %} + <li><a href="{{ link }}">{{ title }}</a></li> + {% endfor %} + + {% if DISPLAY_PAGES_ON_MENU -%} + {% for pg in pages %} + <li{% if pg == page %} class="active"{% endif %}><a href="{{ SITEURL }}/{{ pg.url }}">{{ pg.title }}</a></li> + {% endfor %} + {% endif %} + + {% if DISPLAY_CATEGORIES_ON_MENU -%} + {% for cat, null in categories %} + <li{% if cat == category %} class="active"{% endif %}><a href="{{ SITEURL }}/{{ cat.url }}">{{ cat }}</a></li> + {% endfor %} + {% endif %} + + {% if FEED_ALL_ATOM %} + <li class="right" style="--color: #ef5226"><a href="{{ FEED_DOMAIN }}/{% if FEED_ALL_ATOM_URL %}{{ FEED_ALL_ATOM_URL }}{% else %}{{ FEED_ALL_ATOM }}{% endif %}" type="application/atom+xml" rel="alternate">atom</a></li> + {% endif %} + + {% if FEED_ALL_RSS %} + <li class="right" style="--color: #ef5226"><a href="{{ FEED_DOMAIN }}/{% if FEED_ALL_RSS_URL %}{{ FEED_ALL_RSS_URL }}{% else %}{{ FEED_ALL_RSS }}{% endif %}" type="application/rss+xml" rel="alternate">rss</a></li> + {% endif %} + </ul></nav> + </header> + + <main id="top"> + {% block content %} + {% endblock %} + </main> + + <footer id="contentinfo" class="body"> + <p>Proudly powered by <a rel="nofollow" href="https://getpelican.com/">Pelican</a>, which takes great advantage of <a rel="nofollow" href="https://www.python.org/">Python</a>.<br> + Various background tiles from <a href="https://background-tiles.com/">background-tiles.com</a>.</p> + </footer> + </body> +</html> |