eleventy-plugin-podcaster 1.4.0 → 2.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,13 +38,16 @@ export default function (eleventyConfig) {
38
38
 
39
39
  ## ➡ [Documentation and usage][Podcaster]
40
40
 
41
- Detailed and specific information about how to install and use **Podcaster** can be found in [the Documentation section](https://eleventy-plugin-podcaster.com/docs) of [the `eleventy-plugin-podcaster` site][Podcaster], but here's a quick summary.
41
+ [Podcaster]: https://eleventy-plugin-podcaster.com/docs
42
42
 
43
- [Podcaster]: https://eleventy-plugin-podcaster.com
43
+ Detailed and specific information about how to install and use **Podcaster** can be found in [the Documentation section](docs/index.md) of the site, but here's a quick summary.
44
44
 
45
- **Podcaster** is an Eleventy plugin. You install it in your config file in the usual way. You usually provide it with information about your podcast — like its title, description and category — by creating a `podcast.json` file in the data directory. For each episode, you create a template with information in the front matter about that episode — its name, release date, filename, duration and so on.
46
-
47
- Once you do this, **Podcaster** can create the RSS feed for your podcast. You can also create templates for various pages on your website and include on those pages the information you have provided about the podcast and its episodes.
45
+ 1. **Podcaster** is an Eleventy plugin. Create an Eleventy site and install the `eleventy-plugin-podcaster` plugin in the usual way.
46
+ 2. In the data directory, create a `podcast.json` file. This will contain information about your podcast and its site — at the very least, its title, the URL of the site, a description, its language, and its category.
47
+ 3. In the input directory, create a `episodeFiles` directory and put your podcast MP3s in there.
48
+ 4. In the input directory, create a `episodePosts` directory. You will have a post for each episode, and that post will include information about the episode in its filename and front matter and will have as its content the episode description or show notes.
49
+ 5. Create pages on your site for each of your episodes using the posts in the `episodePosts` directory. You can also create index pages, topic pages, guest pages or anything you like, using the information you have supplied to **Podcaster**.
50
+ 6. Find a host for your site, and a CDN for your podcast episodes.
48
51
 
49
52
  > [!WARNING]
50
- > **Podcaster** only works with Node 20 and later.
53
+ > **Podcaster** requires Node 20 or later.
package/ava.config.js ADDED
@@ -0,0 +1,3 @@
1
+ export default {
2
+ files: ['test/**/*', '!test/v1']
3
+ }
@@ -1,136 +1,37 @@
1
- import { DateTime, Duration } from 'luxon'
2
- import hr from '@tsmx/human-readable'
3
- import rssPlugin from '@11ty/eleventy-plugin-rss'
4
- import { readFileSync } from 'node:fs'
5
- import path from 'node:path'
1
+ import podcastFeed from './src/podcastFeed.js'
2
+ import podcastData from './src/podcastData.js'
3
+ import episodeData from './src/episodeData.js'
6
4
  import calculateFilenameSizeAndDuration from './src/calculateFilenameSizeAndDuration.js'
5
+ import readableFilters from './src/readableFilters.js'
7
6
  import excerpts from './src/excerpts.js'
8
7
  import drafts from './src/drafts.js'
8
+ import pageTitle from './src/pageTitle.js'
9
9
 
10
10
  export default function (eleventyConfig, options = {}) {
11
- if (!('addTemplate' in eleventyConfig)) {
12
- console.log('[eleventy-plugin-podcasting] WARN Eleventy plugin compatibility: Virtual Templates are required for this plugin — please use Eleventy v3.0 or newer.')
13
- }
11
+ eleventyConfig.addPlugin(podcastFeed, options)
12
+ eleventyConfig.addPlugin(podcastData, options)
13
+ eleventyConfig.addPlugin(episodeData, options)
14
+ eleventyConfig.addPlugin(calculateFilenameSizeAndDuration, options)
14
15
 
15
- eleventyConfig.addPlugin(rssPlugin, {
16
- posthtmlRenderOptions: {
17
- closingSingleTag: 'default' // opt-out of <img/>-style XHTML single tags
18
- }
19
- })
16
+ // Filters
20
17
 
21
- eleventyConfig.addGlobalData('eleventyComputed.podcast.feedPath', () => {
22
- return data => data.podcast.feedPath || '/feed/podcast.xml'
23
- })
18
+ eleventyConfig.addPlugin(readableFilters, options)
24
19
 
25
- eleventyConfig.addGlobalData('eleventyComputed.podcast.imagePath', () => {
26
- return data => data.podcast.imagePath || '/img/podcast-logo.jpg'
27
- })
20
+ // Optional features
28
21
 
29
- function calculateCopyrightNotice () {
30
- return data => {
31
- const thisYear = DateTime.now().year
32
- let yearRange
33
- if (!data.podcast.startingYear || data.podcast.startingYear === thisYear) {
34
- yearRange = thisYear
35
- } else {
36
- yearRange = `${data.podcast.startingYear}–${thisYear}`
37
- }
38
- return `© ${yearRange} ${data.podcast.copyright || data.podcast.author}`
39
- }
22
+ if (options.optionalFeatures) {
23
+ options.handleDrafts = true
24
+ options.handleExcerpts = true
25
+ options.handlePageTitle ??= true // preserve setting for custom separators
40
26
  }
41
27
 
42
- eleventyConfig.addGlobalData(
43
- 'eleventyComputed.podcast.copyrightNotice',
44
- calculateCopyrightNotice
45
- )
46
-
47
- eleventyConfig.addGlobalData(
48
- 'eleventyComputed.copyrightNotice',
49
- calculateCopyrightNotice
50
- )
51
-
52
- eleventyConfig.addGlobalData(
53
- 'podcast.feedLastBuildDate',
54
- DateTime.now().toRFC2822()
55
- )
56
-
57
- eleventyConfig.addGlobalData('eleventyComputed.episode.url', () => {
58
- return data => {
59
- if (!data.tags?.includes('podcastEpisode')) return
60
-
61
- const episodeUrlBase = data.podcast.episodeUrlBase
62
- const filename = data.episode.filename
63
- return new URL(filename, episodeUrlBase).toString()
64
- }
65
- })
66
-
67
- eleventyConfig.addShortcode('year', () => DateTime.now().year)
68
-
69
- if (options.readableDateLocale) {
70
- eleventyConfig.addFilter('readableDate', function (date) {
71
- if (date instanceof Date) {
72
- date = date.toISOString()
73
- }
74
- const result = DateTime.fromISO(date, {
75
- zone: 'UTC'
76
- })
77
- return result.setLocale(options.readableDateLocale).toLocaleString(DateTime.DATE_HUGE)
78
- })
28
+ if (options.handleExcerpts) {
29
+ eleventyConfig.addPlugin(excerpts, options)
79
30
  }
80
-
81
- if (options.calculatePageTitle) {
82
- const separator = options.calculatePageTitle === true ? '&middot;' : options.calculatePageTitle
83
-
84
- eleventyConfig.addGlobalData('eleventyComputed.pageTitle', () => {
85
- return data => {
86
- const siteTitle = data.site?.title || data.podcast.title
87
- if (data.title && data.title.length > 0 && data.title !== siteTitle) {
88
- return `${data.title} ${separator} ${siteTitle}`
89
- } else {
90
- return siteTitle
91
- }
92
- }
93
- })
31
+ if (options.handleDrafts) {
32
+ eleventyConfig.addPlugin(drafts, options)
94
33
  }
95
-
96
- eleventyConfig.addFilter('readableDuration', (seconds, omitLeadingZero) => {
97
- if (!seconds) return '0:00:00'
98
- if (omitLeadingZero && seconds < 3600) {
99
- return Duration.fromMillis(seconds * 1000).toFormat('mm:ss')
100
- }
101
- return Duration.fromMillis(seconds * 1000).toFormat('h:mm:ss')
102
- })
103
-
104
- eleventyConfig.addFilter('readableSize', (bytes, fixedPrecision) =>
105
- (fixedPrecision)
106
- ? hr.fromBytes(bytes, { fixedPrecision })
107
- : hr.fromBytes(bytes)
108
- )
109
-
110
- const podcastFeedPath = path.join(import.meta.dirname, './src/podcastFeed.njk')
111
-
112
- eleventyConfig.addTemplate('feed.njk', readFileSync(podcastFeedPath), {
113
- eleventyExcludeFromCollections: true,
114
- eleventyImport: {
115
- collections: ['podcastEpisode']
116
- }
117
- })
118
-
119
- if (options.handleEpisodePermalinks) {
120
- eleventyConfig.addGlobalData('eleventyComputed.permalink', () => {
121
- return data => {
122
- if (data.tags?.includes('podcastEpisode')) {
123
- if (data.episode?.seasonNumber) {
124
- return `/s${data.episode.seasonNumber}/e${data.episode.episodeNumber}/`
125
- } else {
126
- return `/${data.episode.episodeNumber}/`
127
- }
128
- }
129
- }
130
- })
34
+ if (options.handlePageTitle) {
35
+ eleventyConfig.addPlugin(pageTitle, options)
131
36
  }
132
-
133
- eleventyConfig.addPlugin(calculateFilenameSizeAndDuration, options)
134
- eleventyConfig.addPlugin(excerpts, options)
135
- eleventyConfig.addPlugin(drafts, options)
136
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eleventy-plugin-podcaster",
3
- "version": "1.4.0",
3
+ "version": "2.0.0-alpha.0",
4
4
  "description": "An Eleventy plugin that allows you to create a podcast and its accompanying website",
5
5
  "main": "eleventy.config.js",
6
6
  "exports": {
@@ -10,7 +10,7 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/nathan-bottomley/eleventy-plugin-podcaster.git"
12
12
  },
13
- "homepage": "https://github.com/nathan-bottomley/eleventy-plugin-podcaster/tree/main/docs",
13
+ "homepage": "https://eleventy-plugin-podcaster.com",
14
14
  "type": "module",
15
15
  "scripts": {
16
16
  "test": "ava"
@@ -36,7 +36,7 @@
36
36
  "htmlparser2": "^9.1.0",
37
37
  "luxon": "^3.4.4",
38
38
  "markdown-it": "^14.1.0",
39
- "mp3-duration": "^1.1.0"
39
+ "music-metadata": "^11.7.1"
40
40
  },
41
41
  "devDependencies": {
42
42
  "ava": "^6.1.3",
@@ -1,8 +1,8 @@
1
1
  import { Duration } from 'luxon'
2
2
  import path from 'node:path'
3
3
  import { existsSync } from 'node:fs'
4
- import { readdir, stat, readFile, writeFile } from 'node:fs/promises'
5
- import mp3Duration from 'mp3-duration'
4
+ import { readdir, stat, writeFile } from 'node:fs/promises'
5
+ import { parseFile } from 'music-metadata'
6
6
  import hr from '@tsmx/human-readable'
7
7
  import chalk from 'chalk'
8
8
 
@@ -11,14 +11,13 @@ const convertSecondsToReadableDuration = seconds =>
11
11
  .shiftTo('days', 'hours', 'minutes', 'seconds')
12
12
  .toHuman()
13
13
 
14
- export default function (eleventyConfig, options = {}) {
14
+ export default function (eleventyConfig) {
15
15
  let firstRun = true
16
- eleventyConfig.on('eleventy.before', async ({ dir, directories }) => {
16
+ eleventyConfig.on('eleventy.before', async ({ directories }) => {
17
17
  // don't keep recalculating episode data in serve mode
18
18
  if (!firstRun || process.env.SKIP_EPISODE_CALCULATIONS === 'true') return
19
19
  firstRun = false
20
-
21
- const episodesDir = path.join(process.cwd(), options.episodesDir || 'episodes')
20
+ const episodesDir = path.join(directories.input, 'episodeFiles')
22
21
  if (!existsSync(episodesDir)) return
23
22
 
24
23
  const episodes = await readdir(episodesDir)
@@ -34,8 +33,8 @@ export default function (eleventyConfig, options = {}) {
34
33
  const episodePath = path.join(episodesDir, episode)
35
34
  const episodeSize = (await stat(episodePath)).size
36
35
  totalSize += episodeSize
37
- const buffer = await readFile(episodePath)
38
- const episodeDuration = await mp3Duration(buffer)
36
+ const episodeMetadata = await parseFile(episodePath, { duration: true })
37
+ const episodeDuration = episodeMetadata.format.duration
39
38
  totalDuration += episodeDuration
40
39
  episodesData[episode] = {
41
40
  size: episodeSize,
@@ -44,40 +43,50 @@ export default function (eleventyConfig, options = {}) {
44
43
  }
45
44
 
46
45
  const dataDir = path.join(process.cwd(), directories.data)
47
- await writeFile(path.join(dataDir, 'episodesData.json'), JSON.stringify(episodesData, null, 2))
46
+ await writeFile(path.join(dataDir, 'episodeData.json'), JSON.stringify(episodesData, null, 2))
48
47
 
49
48
  console.log(chalk.yellow(`${totalEpisodes} episodes; ${hr.fromBytes(totalSize)}; ${convertSecondsToReadableDuration(totalDuration)}.`))
50
49
  })
51
50
 
52
- if (options.episodeFilenamePattern) {
53
- eleventyConfig.addGlobalData('eleventyComputed.episode.filename', () => {
54
- return data => {
55
- if (data.episode.filename) return data.episode.filename
51
+ const filenameSeasonAndEpisodePattern =
52
+ /^.*?\b[sS](?<seasonNumber>\d+)[eE](?<episodeNumber>\d+)\b.*\.mp3$/
53
+ const filenameEpisodePattern = /^.*?\b(?<episodeNumber>\d+)\b.*\.mp3$/
54
+
55
+ eleventyConfig.addGlobalData('eleventyComputed.episode.filename', () => {
56
+ return data => {
57
+ if (data.episode.filename) return data.episode.filename
56
58
 
57
- if (data.tags?.includes('podcastEpisode') && data.episodesData) {
58
- for (const file of Object.keys(data.episodesData)) {
59
- const match = file.match(options.episodeFilenamePattern)
60
- const matchedSeasonNumber = parseInt(match?.groups.seasonNumber)
61
- const matchedEpisodeNumber = parseInt(match?.groups.episodeNumber)
62
- if (isNaN(matchedSeasonNumber) && matchedEpisodeNumber ===
63
- data.episode.episodeNumber) {
59
+ if (!data.page.inputPath.includes('/episodePosts/')) return
60
+
61
+ for (const file of Object.keys(data.episodeData)) {
62
+ if (data.episode.seasonNumber && data.episode.episodeNumber) {
63
+ const seasonAndEpisodeMatch = file.match(filenameSeasonAndEpisodePattern)
64
+ if (seasonAndEpisodeMatch) {
65
+ const matchedSeasonNumber = parseInt(seasonAndEpisodeMatch.groups.seasonNumber)
66
+ const matchedEpisodeNumber = parseInt(seasonAndEpisodeMatch.groups.episodeNumber)
67
+ if (matchedSeasonNumber === data.episode.seasonNumber &&
68
+ matchedEpisodeNumber === data.episode.episodeNumber) {
64
69
  return file
65
- } else if (matchedSeasonNumber === data.episode.seasonNumber &&
66
- matchedEpisodeNumber === data.episode.episodeNumber) {
70
+ }
71
+ }
72
+ } else if (data.episode.episodeNumber) {
73
+ const episodeMatch = file.match(filenameEpisodePattern)
74
+ if (episodeMatch) {
75
+ const matchedEpisodeNumber = parseInt(episodeMatch.groups.episodeNumber)
76
+ if (matchedEpisodeNumber === data.episode.episodeNumber) {
67
77
  return file
68
78
  }
69
79
  }
70
80
  }
71
81
  }
72
- })
73
- }
82
+ }
83
+ })
74
84
 
75
85
  eleventyConfig.addGlobalData('eleventyComputed.episode.size', () => {
76
86
  return data => {
77
87
  if (data.episode.size) return data.episode.size
78
-
79
- if (data.tags?.includes('podcastEpisode') && data.episodesData) {
80
- return data.episodesData[data.episode.filename]?.size
88
+ if (data.page.inputPath.includes('/episodePosts/') && data.episodeData) {
89
+ return data.episodeData[data.episode.filename]?.size
81
90
  }
82
91
  }
83
92
  })
@@ -86,8 +95,8 @@ export default function (eleventyConfig, options = {}) {
86
95
  return data => {
87
96
  if (data.episode.duration) return data.episode.duration
88
97
 
89
- if (data.tags?.includes('podcastEpisode') && data.episodesData) {
90
- return data.episodesData[data.episode.filename]?.duration
98
+ if (data.page.inputPath.includes('/episodePosts/') && data.episodeData) {
99
+ return data.episodeData[data.episode.filename]?.duration
91
100
  }
92
101
  }
93
102
  })
@@ -0,0 +1,59 @@
1
+ export default function (eleventyConfig, options = {}) {
2
+ const postFilenameSeasonAndEpisodePattern =
3
+ /^[sS](?<seasonNumber>\d+)[eE](?<episodeNumber>\d+)/i
4
+ const postFilenameEpisodePattern = /^(?:e|ep|episode-)(?<episodeNumber>\d+)/i
5
+
6
+ eleventyConfig.addGlobalData('eleventyComputed.episode.seasonNumber', () => {
7
+ return data => {
8
+ if (data.episode?.seasonNumber) return data.episode.seasonNumber
9
+
10
+ if (!data.page.inputPath.includes('/episodePosts/')) return
11
+
12
+ const seasonAndEpisodeMatch = data.page.fileSlug.match(postFilenameSeasonAndEpisodePattern)
13
+ if (seasonAndEpisodeMatch) {
14
+ return parseInt(seasonAndEpisodeMatch.groups.seasonNumber, 10)
15
+ }
16
+ }
17
+ })
18
+
19
+ eleventyConfig.addGlobalData('eleventyComputed.episode.episodeNumber', () => {
20
+ return data => {
21
+ if (data.episode?.episodeNumber) return data.episode.episodeNumber
22
+
23
+ if (!data.page.inputPath.includes('/episodePosts/')) return
24
+
25
+ const seasonAndEpisodeMatch = data.page.fileSlug.match(postFilenameSeasonAndEpisodePattern)
26
+ if (seasonAndEpisodeMatch) {
27
+ return parseInt(seasonAndEpisodeMatch.groups.episodeNumber, 10)
28
+ }
29
+ const episodeMatch = data.page.fileSlug.match(postFilenameEpisodePattern)
30
+ if (episodeMatch) {
31
+ return parseInt(episodeMatch.groups.episodeNumber, 10)
32
+ } else {
33
+ console.error(`[eleventy-plugin-podcaster] Cannot determine episode number for ${data.page.inputPath}. Please ensure the file slug contains a number or set the episodeNumber explicitly in the front matter.`)
34
+ }
35
+ }
36
+ })
37
+
38
+ eleventyConfig.addGlobalData('eleventyComputed.permalink', () => {
39
+ return data => {
40
+ if (data.permalink) return data.permalink
41
+
42
+ if (data.episode?.seasonNumber && data.episode?.episodeNumber) {
43
+ return `/s${data.episode.seasonNumber}/e${data.episode.episodeNumber}/`
44
+ } else if (data.episode?.episodeNumber) {
45
+ return `/${data.episode.episodeNumber}/`
46
+ }
47
+ }
48
+ })
49
+
50
+ eleventyConfig.addGlobalData('eleventyComputed.episode.url', () => {
51
+ return data => {
52
+ if (!data.page.inputPath.includes('/episodePosts/')) return
53
+
54
+ const episodeUrlBase = data.podcast.episodeUrlBase
55
+ const filename = data.episode.filename
56
+ return URL.parse(filename, episodeUrlBase)
57
+ }
58
+ })
59
+ }
package/src/excerpts.js CHANGED
@@ -4,10 +4,8 @@ import markdownIt from 'markdown-it'
4
4
 
5
5
  export default function (eleventyConfig, options = {}) {
6
6
  eleventyConfig.addGlobalData('eleventyComputed.excerpt', () => {
7
- if (!options.handleExcerpts) return
8
-
9
7
  return (data) => {
10
- if (!data.tags?.includes('podcastEpisode')) return
8
+ if (!data.page.inputPath.includes('/episodePosts/')) return
11
9
 
12
10
  const md = markdownIt({
13
11
  html: true,
@@ -0,0 +1,16 @@
1
+ export default function (eleventyConfig, options = {}) {
2
+ const separator = (options.handlePageTitle === true)
3
+ ? '&middot;'
4
+ : options.handlePageTitle
5
+
6
+ eleventyConfig.addGlobalData('eleventyComputed.pageTitle', () => {
7
+ return data => {
8
+ const siteTitle = data.site?.title || data.podcast.title
9
+ if (data.title && data.title.length > 0 && data.title !== siteTitle) {
10
+ return `${data.title} ${separator} ${siteTitle}`
11
+ } else {
12
+ return siteTitle
13
+ }
14
+ }
15
+ })
16
+ }
@@ -0,0 +1,39 @@
1
+ import { DateTime } from 'luxon'
2
+
3
+ export default function (eleventyConfig) {
4
+ eleventyConfig.addGlobalData('eleventyComputed.podcast.feedPath', () => {
5
+ return data => data.podcast.feedPath || '/feed/podcast.xml'
6
+ })
7
+
8
+ eleventyConfig.addGlobalData('eleventyComputed.podcast.imagePath', () => {
9
+ return data => data.podcast.imagePath || '/img/podcast-logo.jpg'
10
+ })
11
+
12
+ eleventyConfig.addGlobalData('eleventyComputed.podcast.episodeUrlBase', () => {
13
+ return data => {
14
+ if (data.podcast.episodeUrlBase) return data.podcast.episodeUrlBase
15
+ let siteUrl
16
+ try {
17
+ siteUrl = data.podcast.siteUrl || data.site.url
18
+ } catch (e) {
19
+ console.error('[eleventy-plugin-podcaster] No site URL found. Please set `siteUrl` in your podcast data.')
20
+ }
21
+ return URL.parse('episodes/', siteUrl)
22
+ }
23
+ })
24
+
25
+ eleventyConfig.addGlobalData('eleventyComputed.podcast.copyrightNotice', () => {
26
+ return data => {
27
+ const thisYear = DateTime.now().year
28
+ let yearRange
29
+ if (!data.podcast.startingYear || data.podcast.startingYear === thisYear) {
30
+ yearRange = thisYear
31
+ } else {
32
+ yearRange = `${data.podcast.startingYear}–${thisYear}`
33
+ }
34
+ return `© ${yearRange} ${data.podcast.copyright || data.podcast.author}`
35
+ }
36
+ })
37
+
38
+ eleventyConfig.addGlobalData('podcast.feedLastBuildDate', DateTime.now().toRFC2822())
39
+ }
@@ -0,0 +1,28 @@
1
+ import rssPlugin from '@11ty/eleventy-plugin-rss'
2
+ import { readFileSync } from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ export default function (eleventyConfig, options = {}) {
6
+ if (!('addTemplate' in eleventyConfig)) {
7
+ console.error('[eleventy-plugin-podcasting] Eleventy plugin compatibility: Virtual Templates are required for this plugin — please use Eleventy v3.0 or newer.')
8
+ }
9
+
10
+ const podcastFeedPath = path.join(import.meta.dirname, './podcastFeed.njk')
11
+
12
+ eleventyConfig.addTemplate('feed.njk', readFileSync(podcastFeedPath), {
13
+ eleventyExcludeFromCollections: true,
14
+ eleventyImport: {
15
+ collections: ['podcastEpisode']
16
+ }
17
+ })
18
+
19
+ eleventyConfig.addPlugin(rssPlugin, {
20
+ posthtmlRenderOptions: {
21
+ closingSingleTag: 'default' // opt-out of <img/>-style XHTML single tags
22
+ }
23
+ })
24
+
25
+ eleventyConfig.addCollection('podcastEpisode', (collectionApi) => {
26
+ return collectionApi.getFilteredByGlob('**/episodePosts/*')
27
+ })
28
+ }
@@ -0,0 +1,27 @@
1
+ import { DateTime, Duration } from 'luxon'
2
+ import hr from '@tsmx/human-readable'
3
+
4
+ export default function (eleventyConfig, options = {}) {
5
+ eleventyConfig.addFilter('readableDate', function (date) {
6
+ const readableDateLocale = options.readableDateLocale ?? 'en-AU'
7
+ if (date instanceof Date) {
8
+ date = date.toISOString()
9
+ }
10
+ const result = DateTime.fromISO(date, {
11
+ zone: 'UTC'
12
+ })
13
+ return result.setLocale(readableDateLocale).toLocaleString(DateTime.DATE_HUGE)
14
+ })
15
+
16
+ eleventyConfig.addFilter('readableDuration', (seconds) => {
17
+ if (!seconds) return '0:00:00'
18
+ if (seconds < 3600) {
19
+ return Duration.fromMillis(seconds * 1000).toFormat('mm:ss')
20
+ }
21
+ return Duration.fromMillis(seconds * 1000).toFormat('h:mm:ss')
22
+ })
23
+
24
+ eleventyConfig.addFilter('readableSize', (bytes, fixedPrecision = 1) =>
25
+ hr.fromBytes(bytes, { fixedPrecision })
26
+ )
27
+ }