From b5bf9bc4ca6425871ee2567849c3a2dd674370c7 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 2 Oct 2024 08:54:11 +0100 Subject: [PATCH] feat(#324): refactor collections for topics --- lib/collections.js | 157 ++++++++++++++---- src/_includes/components/side-bars/topic.njk | 4 +- src/_includes/components/side-bars/topics.njk | 22 ++- .../taxonomy/series/series.11tydata.js | 27 +++ src/topic/topic.njk | 19 +-- 5 files changed, 177 insertions(+), 52 deletions(-) create mode 100644 src/content/taxonomy/series/series.11tydata.js diff --git a/lib/collections.js b/lib/collections.js index 6e88f981..f85242a5 100644 --- a/lib/collections.js +++ b/lib/collections.js @@ -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"; @@ -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/', @@ -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); diff --git a/src/_includes/components/side-bars/topic.njk b/src/_includes/components/side-bars/topic.njk index c2bc2b48..438851d7 100644 --- a/src/_includes/components/side-bars/topic.njk +++ b/src/_includes/components/side-bars/topic.njk @@ -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 %} diff --git a/src/_includes/components/side-bars/topics.njk b/src/_includes/components/side-bars/topics.njk index e6181818..cb5b263a 100644 --- a/src/_includes/components/side-bars/topics.njk +++ b/src/_includes/components/side-bars/topics.njk @@ -1,11 +1,25 @@ -{% set topics = collections | tagsInCollection(['writing'], ['stage/stub']) %}
-

All Topics ({{ topics.length }})

+

All Topics ({{ collections.topics.length }})

    - {% for topic in topics %} -
  1. {{ topic.name }} ({{ topic.usages }})
  2. + {% for topic in collections.topics %} + {% if topic.isSpecialTag === false %} +
  3. {{ topic.name }} ({{ topic.items.length }})
  4. + {% endif %} + {% endfor %} +
+
+ +
+
+

All Topics ({{ collections.topics.length }})

+
+
    + {% for topic in collections.topics %} + {% if topic.isSpecialTag %} +
  1. {{ topic.name }} ({{ topic.items.length }})
  2. + {% endif %} {% endfor %}
diff --git a/src/content/taxonomy/series/series.11tydata.js b/src/content/taxonomy/series/series.11tydata.js new file mode 100644 index 00000000..76edc536 --- /dev/null +++ b/src/content/taxonomy/series/series.11tydata.js @@ -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}/`; + }, + }, +} diff --git a/src/topic/topic.njk b/src/topic/topic.njk index 07e19ff6..3602fd18 100644 --- a/src/topic/topic.njk +++ b/src/topic/topic.njk @@ -1,31 +1,24 @@ ---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 %} @@ -33,7 +26,7 @@ {% block content %}
-

{{ topic }}

+

{{ topic.name }}

Hello world