]> Witch of Git - web/blog/blob - eleventy.config.js
Make the atom feed always include an updated element
[web/blog] / eleventy.config.js
1 import pluginRss from "@11ty/eleventy-plugin-rss";
2 import pluginSyntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
3 import uslug from "uslug";
4 import anchor from "markdown-it-anchor";
5 import markdownIt from "markdown-it";
6 import CleanCSS from "clean-css";
7 import htmlMinifier from "html-minifier-terser";
8 import util from "util";
9 import { DateTime } from "luxon";
10
11 const md = markdownIt({
12 html: true,
13 typographer: true,
14 }).use(anchor, {
15 slugify: s => uslug(s),
16 permalink: true,
17 permalinkBefore: true,
18 permalinkClass: "header-anchor c-sun dec-none",
19 });
20
21 export default async function (eleventyConfig) {
22 eleventyConfig.addDateParsing(parseDate);
23 // @TODO: Update to the new virtual template setup?
24 eleventyConfig.addPlugin(pluginRss);
25 eleventyConfig.addPlugin(pluginSyntaxHighlight, {
26 errorOnInvalidLanguage: true,
27 });
28
29 eleventyConfig.setLibrary("md", md);
30
31 eleventyConfig.addTransform("html-minifier", htmlMinifierTransform);
32
33 eleventyConfig.addPassthroughCopy("img");
34 eleventyConfig.addPassthroughCopy("static");
35 eleventyConfig.addPassthroughCopy("stamps");
36 eleventyConfig.addPassthroughCopy({ "favicon": "/" });
37
38 eleventyConfig.addNunjucksShortcode("youtube", youtubeShortcode);
39 eleventyConfig.addPairedNunjucksShortcode("tweet", tweetShortcode);
40 eleventyConfig.addPairedShortcode("aside", asideShortcode);
41 eleventyConfig.addPairedShortcode("figure", figureShortcode);
42
43 eleventyConfig.addFilter("date", dateFilter);
44 eleventyConfig.addFilter("markdown", value => md.renderInline(value));
45 eleventyConfig.addFilter("groupby", groupbyFilter);
46 eleventyConfig.addFilter("cssmin", css =>
47 new CleanCSS({}).minify(css).styles);
48 eleventyConfig.addFilter("debug", util.inspect);
49 eleventyConfig.addFilter("toRfc3339", toRfc3339Filter);
50 eleventyConfig.addFilter("hasTime", hasTimeFilter);
51
52 eleventyConfig.setDataDeepMerge(true);
53
54 eleventyConfig.addCollection("years", collection => {
55 const posts = collection.getFilteredByTag("posts");
56 const items = groupby(posts, item => item.date.getFullYear());
57 return items.reduce((obj, [k, v]) => (obj[k] = v, obj), {});
58 });
59
60 return {
61 markdownTemplateEngine: "njk",
62 };
63 };
64
65 /**
66 * @param {{string | Date | DateTime}} anyDate
67 * @returns {DateTime}
68 */
69 function dateTime(anyDate) {
70 if (anyDate instanceof Date) {
71 return DateTime.fromJSDate(anyDate);
72 }
73 if (DateTime.isDateTime(anyDate)) {
74 return anyDate;
75 }
76 let date = parseDate(anyDate);
77 if (date == null) { throw Error(`failed to parse date ${anyDate}`); }
78 return date;
79 }
80
81 function dateFilter(date, format) {
82 return dateTime(date).toFormat(format);
83 }
84
85 function toRfc3339Filter(date) {
86 return dateTime(date).toISO({ suppressMilliseconds: true });
87 }
88
89 function hasTimeFilter(date) {
90 date = dateTime(date);
91 return date.hour != 0
92 || date.minute != 0
93 || date.second != 0
94 || date.millisecond != 0;
95 }
96
97 /**
98 * @param {string} dateText
99 * @returns {{DateTime | null}}
100 */
101 function parseDate(dateText) {
102 try {
103 if (!dateText) dateText = "";
104 const formats = [
105 "yyyy-MM-dd HH:mm:ss z",
106 "yyyy-MM-dd z",
107 "yyyy-MM-dd",
108 ];
109 for (const format of formats) {
110 const date = DateTime.fromFormat(dateText, format, { setZone: true });
111 if (date.isValid) { return date; }
112 }
113 } catch {
114 return null;
115 }
116 }
117
118 function access(item, path) {
119 const segments = path.split(".");
120 for (const seg of segments) {
121 if (item === undefined) { return null; }
122 if (seg.endsWith("()")) {
123 const method = item[seg.slice(0, -2)];
124 if (method === undefined) { return null; }
125 item = method.bind(item)();
126 } else {
127 item = item[seg];
128 }
129 }
130 return item;
131 }
132
133 function groupby(items, keyFn) {
134 const results = [];
135 for (const item of items) {
136 const key = keyFn(item);
137 if (results.length == 0 || key != results[results.length-1][0]) {
138 results.push([key, [item]]);
139 } else {
140 results[results.length-1][1].push(item);
141 }
142 }
143 return results;
144 }
145
146 function groupbyFilter(items, path) {
147 return groupby(items, item => access(item, path));
148 }
149
150 function youtubeShortcode(items, inWidth = 560, inHeight = 315) {
151 const width = items.width || inWidth;
152 const height = items.height || inHeight;
153 const allow = items.allow || "accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
154 const border = items.border || "0";
155 const video = items.video || items;
156 if (!video) {
157 throw "Required argument 'video'.";
158 }
159 const src = "https://www.youtube.com/embed/" + video;
160 return `<div class="youtube row hcenter">
161 <iframe width="${width}" height="${height}" src="${src}"
162 frameborder="${border}" allow="${allow}" allowfullscreen></iframe>
163 </div>`;
164 }
165
166 function tweetShortcode(content, items) {
167 // @TODO: Handle parsing date
168 return `<div class="row hcenter">
169 <blockquote class="twitter-tweet">
170 <p lang="en" dir="ltr">${content}</p> &mdash; ${items.name} (@${items.at})
171 <a href="${items.link}">${items.date}</a>
172 </blockquote>
173 <script async src="https://platform.twitter.com/widgets.js" charset="utf-8">
174 </script>
175 </div>`;
176 }
177
178 function asideShortcode(content, style='') {
179 const html = md.render(content);
180 if (style) {
181 return `<aside class="${style}">${html}</aside>`;
182 } else {
183 return `<aside>${html}</aside>`;
184 }
185 }
186
187 function figureShortcode(content, { src, alt }) {
188 const captionHtml = md.render(content);
189 return `<figure class="col hcenter">
190 <img src="${src}" alt="${alt}">
191 <figcaption>${captionHtml}</figcaption>
192 </figure>`;
193 }
194
195 function htmlMinifierTransform(content, outputPath) {
196 if (outputPath.endsWith(".html")) {
197 return htmlMinifier.minify(content, {
198 useShortDoctype: true,
199 removeComments: true,
200 collapseWhitespace: true,
201 });
202 }
203 return content;
204 }