genesis

Update VII

(GO HOME)

Created On: Wed, 18th March (2026)

Prev: Update VI

turns out it’s a bit more complex than i thought.

so first of all, the logic itself. this part was fine.

---
import { getCollection } from 'astro:content';
// ...
let g: bPost[] = await getCollection('type A');
let b: bPost[] = await getCollection('type B');
let s = [...g, ...b];
s = s.map(setup_dates_on_entry);
s = s.sort((a, b) => b.data.createdOn.getTime() - a.data.createdOn.getTime());
---
<Base>
<BgTitle title="sketchbook"/>
<h1>INDEX</h1>
<BlogDir directory={s} client:load/>
</Base>

on the astro part, all i had to do was just get the collections. i also have a collection for all files, however if i use that, all entries inherit that as a collection name so…

there is that little client:load bit there too. apparently by default, Astro will render components as HTML & CSS - which in other words means “pre-rendered”. if you add client:load though, it will include all the client-side javascript. best of both worlds that way.

the most complicated part was the svelte logic itself. so first, a small step.

interface Props {
directory: bPost[];
}
const { directory }: Props = $props();

you’ll need to define the property name twice for that. which is fine but, annoying still.

after that i needed to get all of the relevant tags and collections. i didn’t want to hardcode that. of course we can just grab all the unique values from the collection’s properties. and after some work, i hit the right way.

first, we start with a utility method that will be the heart of this.

const count_keys = (items: any[]) => {
const counter: { [key: string]: number } = {};
items.forEach((item) => {
if (item in counter) {
counter[item as string] += 1;
} else {
counter[item as string] = 1;
}
});
return counter;
};

if the “key” is new, we set it up. otherwise, we increment. it’s that easy. of course however, these will be disordered. that’s fine, but we can do better. infact, here we also get the count. however objects have no sorting. at least as far as i know. so you need to convert them into lists. which then leads to…

const collections = $derived(
Object.entries(count_keys(directory.map((entry) => entry.collection))).sort(
(a, b) => b[1] - a[1],
),
);
const tags = $derived(
Object.entries(
count_keys(directory.flatMap((entry) => entry.data.tags)),
).sort((a, b) => b[1] - a[1]),
);

in both cases, we use count_keys to get the object of counts, and Object.entries to turn the object into a list of tuples --- the first being the key, the last being the value.

now for collections this is easy, we just map all of the entries to their listed collection.

for tags, flatMap saves the day. just incase, entry.data.tags is a list of strings. knowing that, flatMap is an enhanced map that also flattens the lists. or to put it another way…

const list_of_lists = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
list_of_lists.map(e => e); // [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
list_of_lists.flatMap(e => e); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

after that, we just sort by the count --- which would be the value, and thus the second element.

FINALLY comes the filtering.

let filter_collection: string = $state(undefined);
let filter_tag: string = $state(undefined);
let entries = $derived.by(() => {
return [...directory]
.filter((entry) => {
return (
(!filter_collection || entry.collection === filter_collection) &&
(!filter_tag || entry.data.tags.includes(filter_tag))
);
});
});

so first, if a filter is undefined, we shouldn’t filter. if it is, then we should.

the above just does that, an elaborate filter. P.S. do not forget about $derived.by(). i did, and it caused one hell of a pain. kinda one of the downsides of runes - more stuff to remember.

of course, you might be asking, how do you set the filter? well, that goes into Svelte logic but luckily for those who don’t know it enough, it’s pretty simple to follow. infact, i’ve technically been showing you portions straight from a .svelte file, they’re just in a <script lang="ts"> tag.

<div style="...">
<button onclick={() => (filter_collection = undefined)} class="button">
all ({entries.length})
</button>
{#each collections as collection}
<button onclick={() => (filter_collection = collection[0])} class="button"
>{collection[0]} ({collection[1]})</button
>
{/each}
</div>
/* ... */
<div style="...">
<button onclick={() => (filter_tag = undefined)} class="button">
all ({entries.length})
</button>
{#each tags as tag}
<button onclick={() => (filter_tag = tag[0])} class="button"
>{tag[0]} ({tag[1]})</button
>
{/each}
</div>

if you know HTML, then you can read most of the above. you have the onclick property, which just has a lambda (anonymous function, AKA a function without a name). and in that lambda, we’re just setting the filter_collection. in the all button, it’s set to undefined to show everything, and in the other button, the tag or collection name.

the rest of what to explain is part of why I love Svelte so much. it’s very readable. loops are just {#each list as things (id)} and any template expression is closed by using the slash {/each}. you include values by just wrapping them in curly braces. wanna actually show curly braces? double them up!

anyways. that’s how all of this works. i’m thinking of making posts with code snippets or what have you.

Log Index