Skip to content

Commit

Permalink
feat(#324): refactor collections for topics
Browse files Browse the repository at this point in the history
  • Loading branch information
carbontwelve committed Oct 2, 2024
1 parent 61de505 commit b5bf9bc
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 52 deletions.
157 changes: 123 additions & 34 deletions lib/collections.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import chunk from './helpers/chunk.js';
import {slugify, padStart, specialTagMeta} from './filters.js';
import {slugify, padStart, specialTagMeta, specialTagValue} from './filters.js';
import listData from '../src/_data/lists-meta.js';
import metadata from "../src/_data/metadata.js";

Expand Down Expand Up @@ -48,21 +48,125 @@ const post = (collection, glob = './src/content/**/*.md') => ((process.env.ELEVE
: [...collection.getFilteredByGlob(glob)].filter((post) => !post.data.draft)
);

// Written for #20, this creates a collection of all tags
// @see https://github.com/photogabble/website/issues/20
const contentTags = (collection) => Array.from(
post(collection).reduce((tags, post) => {
if (post.data.tags) post.data.tags.forEach(tag => !tag.includes('list/') && tags.add(tag));
/**
* All Topics
* Written for #20 (refactored for #324) this creates a collection of all tags.
*
* This is used by the topics sidebar for displaying all topics in order of how many post
* they have, and by topic/topic.njk for generating per topic pages where one does not
* already exist in src/content/topics.
*
* @see https://github.com/photogabble/website/issues/20
* @see https://github.com/photogabble/website/issues/324
* @param {*} collection
* @return {Array<*>}
*/
const topics = (collection) => {
// IndexPages are the main landing page for a topic, list or series; if they exist
// then the pages frontmatter will be used as data source for the topic.
const indexPages = [
...collection.getFilteredByTag('type/topic'),
...collection.getFilteredByTag('type/list'),
...collection.getFilteredByTag('type/stage'),
...collection.getFilteredByTag('type/series'),
...collection.getFilteredByTag('type/index'),
].reduce((carry, post) => {
switch (specialTagValue(post?.data?.tags, 'type')) {
case 'topic':
carry.set((post.data?.topic ?? post.data.title), post);
break;
case 'stage':
carry.set(`stage/${post.fileSlug}`, post);
break;
case 'list':
carry.set(`list/${(post.data?.list_slug ?? post.fileSlug)}`, post);
break;
case 'series':
carry.set(`series/${post.fileSlug}`, post);
break;
case 'index':
(post.data?.topic ?? post.data?.sidebar_topic) &&
carry.set((post.data?.topic ?? post.data?.sidebar_topic), post);
break;
}
return carry;
}, new Map());

// Some topics can have different ways of being referenced such as "Blogging" and "blogging"
// being the same. While I might also want to merge some topics under one banner such as
// "GameDev" and "GameDevelopment" -> "Game Development". In those cases a topic file
// will be created which lists the topic_aliases for its topic.
const topicMapping = collection.getFilteredByTag('type/topic').reduce((carry, post) => {
// Topics that are set by a TopicIndexPage define the default representation for a topic.
// For example, I might have a TopicIndexPage titled: "Game Development", it can then
// define topic_aliases to say that "GameDev" and "GameDevelopment" reference it.
const topic = (post.data?.topic ?? post.data.title);

[
...(post.data?.topic_aliases ?? []),
topic.toLowerCase(),
topic.replace(/\s/g, '').toLowerCase(),
].forEach(t => carry.set(t, topic));

return carry;
}, new Map());

// Set of unique tags that have been used in within all pages.
const usedTopicSet = Array.from(post(collection).reduce((tags, post) => {
post.data.tags && post.data.tags.forEach(tag => tags.add(tag));
return tags;
}, new Set())
).map(name => {
return {
name,
slug: slugify(name),
items: collection.getFilteredByTag(name).filter((item) => item.data.growthStage && item.data.growthStage !== 'stub').reverse()
}
}).filter(name => name.items.length > 0 && !name.name.includes(':'))
.sort((a, b) => b.items.length - a.items.length);
}, new Set()));

// The usedTopicSet needs to be reduced into a list of topics that take into account the
// topicMapping above.
return Array.from(usedTopicSet.reduce((topics, topic) => {
// Special topics contain a `/` some are canonically different in how they are handled
const isSpecialTag = topic.includes('/');
let name, description, permalink, page;
let items = [];

if (isSpecialTag) {
const key = topic.split('/')[0];
page = indexPages.get(topic);
name = page?.data?.title ?? topic;
permalink = page?.url ?? topic;
items = collection.getFilteredByTag(topic);
} else {
// Normalise the topic for TopicMapping lookup, if no mapping is found then look up
// IndexPage by the raw topic value. If found the IndexPage meta will determine the
// topics title and slug.
const uid = topic.replace(/\s/g, '').toLowerCase();
page = (topicMapping.has(uid))
? indexPages.get(topicMapping.get(uid))
: indexPages.get(topic);
name = page?.data?.title ?? topic;
permalink = page?.url ?? `/topic/${slugify(name)}`;
items = collection.getFilteredByTag(name);
}

description = page?.data?.description;

// const items = collection.getFilteredByTag(name)
// .filter((item) => (item.data?.tags ?? []).some(tag => ['stage/stub'].includes(tag)) === false)
// .reverse();

if (permalink[0] !== '/') permalink = `/${permalink}`;
const record = topics.get(name) ?? {topic, name, description, isSpecialTag, permalink, page, items: []};

record.items = [...record.items, ...items];
topics.set(name, record);

return topics;
}, new Map()).values())
.map(el => {
return {
...el,
items: el.items.filter((item) => (item.data?.tags ?? []).some(tag => ['stage/stub'].includes(tag)) === false),
}
})
.filter(topic => topic.items.length > 0 && topic.name !== 'writing')
.sort((a, b) => b.items.length - a.items.length);
};

const resourcesByType = (collection) => collectSpecialTaggedContent({
prefix: 'resource/',
Expand Down Expand Up @@ -137,25 +241,10 @@ const lists = (collection) => collection.getFilteredByTag('type/list').map(item
* @param collection
* @return {{name: string, description: string, title: string, items: *, slug: string, url: string}[]}
*/
const series = (collection) => collectSpecialTaggedContent('series/', collection);

const topics = (collection) => Array.from(post(collection).reduce((carry, page) => {
if (!page.data?.tags) return carry;

for (const tag of page.data.tags) {
if (tag.includes('/')) continue; // We handle special tags separately
const list = carry.get(tag) ?? {
name: tag,
items: [],
};
list.items.push(page);
carry.set(tag, list);
}

return carry;
}, new Map()).values());


const series = (collection) => collectSpecialTaggedContent({
prefix: 'series/',
collection,
});

export const registerCollections = (eleventyConfig) => {
eleventyConfig.addCollection('lists', lists);
Expand Down
4 changes: 3 additions & 1 deletion src/_includes/components/side-bars/topic.njk
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{% if sidebar_topic === 'stage/stub' %}
{% if sidebar_items %}
{% set items = sidebar_items %}
{% elseif sidebar_topic === 'stage/stub' %}
{% set items = collections[sidebar_topic] | reverse %}
{% else %}
{% set items = collections[sidebar_topic] | notTagged('stage/stub') | reverse %}
Expand Down
22 changes: 18 additions & 4 deletions src/_includes/components/side-bars/topics.njk
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
{% set topics = collections | tagsInCollection(['writing'], ['stage/stub']) %}
<section class="post-list post-list__topics">
<header class="post-list__inline-header">
<h3>All Topics ({{ topics.length }})</h3>
<h3>All Topics ({{ collections.topics.length }})</h3>
</header>
<ol>
{% for topic in topics %}
<li><a href="/topic/{{ topic.name | slugify }}">{{ topic.name }}</a> ({{ topic.usages }})</li>
{% for topic in collections.topics %}
{% if topic.isSpecialTag === false %}
<li><a href="{{ topic.permalink }}">{{ topic.name }}</a> ({{ topic.items.length }})</li>
{% endif %}
{% endfor %}
</ol>
</section>

<section class="post-list post-list__topics">
<header class="post-list__inline-header">
<h3>All Topics ({{ collections.topics.length }})</h3>
</header>
<ol>
{% for topic in collections.topics %}
{% if topic.isSpecialTag %}
<li><a href="{{ topic.permalink }}">{{ topic.name }}</a> ({{ topic.items.length }})</li>
{% endif %}
{% endfor %}
</ol>
</section>
27 changes: 27 additions & 0 deletions src/content/taxonomy/series/series.11tydata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Series and Volumes
*
* This folder contains landing pages for long-running series, the idea here is
* to group serial content such as tutorials under an index page. I also
* want to make use of this for the Week In Review, in which case the different
* years will be different _volumes_ of the same series.
*
*/
export default {
// Do not include in RSS Feed
excludeFromFeed: true,

// Tagged as special topic type, these aren't regular pages
tags: ['type/series'],

// Do not display page meta data
hide_meta: true,
folder: ['series'],

// TODO: complete eleventy computed data to be volume aware?
eleventyComputed: {
permalink(data) {
return `series/${data.page.fileSlug}/`;
},
},
}
19 changes: 6 additions & 13 deletions src/topic/topic.njk
Original file line number Diff line number Diff line change
@@ -1,39 +1,32 @@
---js
{
pagination: {
data: "collections",
data: "collections.topics",
size: 1,
alias: "topic",
before(paginationData, fullData) {
const excluded = (fullData.collections['type/topic'] ?? [])
.reduce((carry, item) => {
const value = item.data?.topic ?? item.data.title;
if (value) carry.add(value);
return carry;
}, new Set(['all']));

return paginationData.filter((topic) => {
return (topic.includes('/') || excluded.has(topic)) === false;
return typeof topic.page === 'undefined';
});
}
},
permalink: "/topic/{{ topic | slugify }}/",
permalink: "{{ topic.permalink }}/",
folder: ['topic'],
}
---
{% extends "layouts/base.njk" %}

{% block sidebar %}
{% set sidebar_title = 'Writing tagged as “' + topic + '' %}
{% set sidebar_topic = topic %}
{% set sidebar_title = 'Writing tagged as “' + topic.name + '' %}
{% set sidebar_items = topic.items %}
{% set displayContentType = true %}
{% include "components/side-bars/topic.njk" %}
{% endblock %}

{% block content %}
<article>
<header>
<h1>{{ topic }}</h1>
<h1>{{ topic.name }}</h1>
</header>
<section>
<p>Hello world</p>
Expand Down

0 comments on commit b5bf9bc

Please sign in to comment.