The fifth version of chriskrycho.com, built in Eleventy.

build: fix types and support `image.cdn` or `image.url`

Changed files
+133 -99
eleventy
site
_includes
components
types
+114
eleventy/data.ts
··· 1 + import { type Dict } from './type-utils'; 2 + 3 + interface ItemData { 4 + title?: string; 5 + subtitle?: string; 6 + summary?: string; 7 + tags?: string[]; 8 + date?: string | Date; 9 + started?: string | Date; 10 + updated?: string | Date; 11 + updates?: Array<Update>; 12 + qualifiers?: Qualifiers; 13 + image?: Image; 14 + link?: string; 15 + splash?: string; 16 + book?: Book; 17 + standalonePage?: boolean; 18 + featured?: boolean; 19 + draft?: boolean; 20 + /** 21 + * Allow overriding the normal feed ID to enable keeping feed entries stable even if 22 + * the slug changes. 23 + */ 24 + feedId?: string; 25 + /** Markdown-enabled thanks to people who contributed to the thing. */ 26 + thanks?: string; 27 + discuss?: { 28 + hn: string; 29 + lobsters: string; 30 + }; 31 + sendEmail?: boolean; 32 + feedOnly?: boolean; 33 + } 34 + 35 + /** Extending the base Eleventy item with my own data */ 36 + declare module '../types/eleventy' { 37 + interface Data extends ItemData {} 38 + } 39 + 40 + type Image = string | { cdn: string } | { url: string }; 41 + 42 + export function imageValue( 43 + data: Pick<ItemData, 'image' | 'book'> | undefined, 44 + ): string | undefined { 45 + if (!data) return undefined; 46 + 47 + if (typeof data.image == 'string') return data.image; 48 + 49 + if (typeof data.image == 'object' && data.image != null) { 50 + console.log(JSON.stringify(data.image)); 51 + let x = 52 + 'cdn' in data.image 53 + ? `https://cdn.chriskrycho.com/images/${data.image.cdn}` 54 + : data.image.url; 55 + console.log(x); 56 + return x; 57 + } 58 + 59 + if (typeof data.book == 'object' && data.book != null) return data.book.cover; 60 + 61 + return undefined; 62 + } 63 + 64 + export type Author = { author: string } | { authors: string[] }; 65 + 66 + export interface Review { 67 + review?: { 68 + rating: 69 + | 'Required' 70 + | 'Recommended' 71 + | 'Recommended With Qualifications' 72 + | 'Not Recommended'; 73 + summary: string; 74 + }; 75 + } 76 + 77 + export interface BookMeta { 78 + title: string; 79 + year?: number | string; 80 + cover?: string; 81 + link?: string; 82 + } 83 + 84 + // Must be a `type` alias because interfaces cannot extend unions. 85 + export type Book = BookMeta & Author & Review; 86 + 87 + export function isBook(maybeBook: unknown): maybeBook is Book { 88 + if (typeof maybeBook !== 'object' || !maybeBook) { 89 + return false; 90 + } 91 + 92 + const maybe = maybeBook as Dict<unknown>; 93 + 94 + return ( 95 + typeof maybe.title === 'string' && 96 + (typeof maybe.author === 'string' || Array.isArray(maybe.authors)) && 97 + (typeof maybe.year == 'number' || typeof maybe.year === 'string') && 98 + typeof maybe.review === 'object' && 99 + typeof maybe.cover === 'string' && 100 + typeof maybe.link === 'string' 101 + ); 102 + } 103 + 104 + export interface Update { 105 + at: string | Date; 106 + changes: string; 107 + } 108 + 109 + export interface Qualifiers { 110 + audience?: string; 111 + context?: string; 112 + epistemic?: string; 113 + discusses?: string[]; 114 + }
+3 -89
eleventy/feed.ts
··· 2 2 import { DateTime } from 'luxon'; 3 3 import { Maybe, Result } from 'true-myth'; 4 4 5 - import { Dict, EleventyClass, Item } from '../types/eleventy'; 5 + import { EleventyClass, Item } from '../types/eleventy'; 6 6 import JsonFeed, { FeedItem } from '../types/json-feed'; 7 7 import absoluteUrl from './absolute-url'; 8 8 import { canParseDate } from './date-time'; ··· 12 12 import markdown from './markdown'; 13 13 import localeDate from './locale-date'; 14 14 import niceList from './nice-list'; 15 + import { type Book, imageValue, isBook, type Qualifiers } from './data'; 15 16 16 17 type BuildInfo = typeof import('../site/_data/build'); 17 18 type SiteConfig = typeof import('../site/_data/config'); ··· 19 20 /** Defensive function in case handed bad data */ 20 21 const optionalString = (value: unknown): string | undefined => 21 22 typeof value === 'string' ? value : undefined; 22 - 23 - type Author = { author: string } | { authors: string[] }; 24 - 25 - interface Review { 26 - review?: { 27 - rating: 28 - | 'Required' 29 - | 'Recommended' 30 - | 'Recommended With Qualifications' 31 - | 'Not Recommended'; 32 - summary: string; 33 - }; 34 - } 35 - 36 - interface BookMeta { 37 - title: string; 38 - year?: number | string; 39 - cover?: string; 40 - link?: string; 41 - } 42 - 43 - // Must be a `type` alias because interfaces cannot extend unions. 44 - type Book = BookMeta & Author & Review; 45 - 46 - interface Update { 47 - at: string | Date; 48 - changes: string; 49 - } 50 - 51 - /** Extending the base Eleventy item with my own data */ 52 - declare module '../types/eleventy' { 53 - interface Data { 54 - title?: string; 55 - subtitle?: string; 56 - summary?: string; 57 - tags?: string[]; 58 - date?: string | Date; 59 - started?: string | Date; 60 - updated?: string | Date; 61 - updates?: Array<Update>; 62 - qualifiers?: Qualifiers; 63 - image?: string; 64 - link?: string; 65 - splash?: string; 66 - book?: Book; 67 - standalonePage?: boolean; 68 - featured?: boolean; 69 - draft?: boolean; 70 - /** 71 - * Allow overriding the normal feed ID to enable keeping feed entries stable even if 72 - * the slug changes. 73 - */ 74 - feedId?: string; 75 - /** Markdown-enabled thanks to people who contributed to the thing. */ 76 - thanks?: string; 77 - discuss?: { 78 - hn: string; 79 - lobsters: string; 80 - }; 81 - sendEmail?: boolean; 82 - feedOnly?: boolean; 83 - } 84 - } 85 - 86 - interface Qualifiers { 87 - audience?: string; 88 - context?: string; 89 - epistemic?: string; 90 - discusses?: string[]; 91 - } 92 - 93 - function isBook(maybeBook: unknown): maybeBook is Book { 94 - if (typeof maybeBook !== 'object' || !maybeBook) { 95 - return false; 96 - } 97 - 98 - const maybe = maybeBook as Dict<unknown>; 99 - 100 - return ( 101 - typeof maybe.title === 'string' && 102 - (typeof maybe.author === 'string' || Array.isArray(maybe.authors)) && 103 - (typeof maybe.year == 'number' || typeof maybe.year === 'string') && 104 - typeof maybe.review === 'object' && 105 - typeof maybe.cover === 'string' && 106 - typeof maybe.link === 'string' 107 - ); 108 - } 109 23 110 24 const joinAuthors = (authors: string[]): Result<string, string> => { 111 25 switch (authors.length) { ··· 305 219 item.data?.updated instanceof Date 306 220 ? isoDate(item.data.updated) 307 221 : undefined, 308 - image: optionalString(item.data?.image ?? item.data?.book?.cover), 222 + image: imageValue(item.data), 309 223 external_url: optionalString(item.data?.link ?? item.data?.book?.link), 310 224 tags: Array.isArray(item.data?.tags) ? item.data?.tags : [], 311 225 banner_image:
+7
eleventy/type-utils.ts
··· 1 + // ---- Utility types 2 + export interface Dict<T = unknown> { 3 + [key: string]: T | undefined; 4 + } 5 + 6 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 + export type AnyFunction<T = any> = (...args: any[]) => T;
+7 -1
site/_includes/components/social.njk
··· 4 4 5 5 {%- set image -%} 6 6 {%- if page.data.image -%} 7 - {{page.data.image}} 7 + {%- if page.data.image.cdn -%} 8 + https://cdn.chriskrycho.com/images/{{page.data.image.cdn}} 9 + {%- elif page.data.image.url -%} 10 + {{page.data.image.url}} 11 + {%- else -%} 12 + {{page.data.image}} 13 + {%- endif -%} 8 14 {%- elif page.data.book.cover -%} 9 15 {{page.data.book.cover}} 10 16 {%- elif pageTitle -%}
+2 -9
types/eleventy.d.ts
··· 1 1 type ServeStaticOptions = import('serve-static').ServeStaticOptions; 2 2 3 - // ---- Utility types 4 - interface Dict<T = unknown> { 5 - [key: string]: T | undefined; 6 - } 7 - 8 - // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 - type AnyFunction<T = any> = (...args: any[]) => T; 10 - 11 3 // ---- Eleventy types 12 4 interface BrowserSyncConfig { 13 5 /** Browsersync includes a user-interface that is accessed via a separate port. The UI allows to controls all devices, push sync updates and much more. */ ··· 67 59 68 60 type Empty = { isEmpty: true; empty: string } | { isEmpty: false }; 69 61 62 + import { AnyFunction, Dict } from '../eleventy/type-utils'; 70 63 import type { GrayMatterFile, GrayMatterOption } from 'gray-matter'; 71 64 72 65 export type Engine = (input: string) => GrayMatterFile<string>; ··· 162 155 In addition to Global Data Files global data can be added to the Eleventy 163 156 config object using the `addGlobalData` method. This is especially useful 164 157 for plugins. 165 - 158 + 166 159 The first value of `addGlobalData` is the key that will be available to 167 160 your templates and the second value is the value of the value returned to 168 161 the template.