eleventy-plugin-podcaster 0.9.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/LICENCE.txt +7 -0
- package/README.md +179 -0
- package/docs/episode-information.md +54 -0
- package/docs/hosting.md +20 -0
- package/docs/optional-features.md +36 -0
- package/docs/podcast-information.md +74 -0
- package/docs/size-and-duration.md +58 -0
- package/eleventy.config.js +91 -0
- package/package.json +38 -0
- package/src/calculateSizeAndDuration.js +70 -0
- package/src/drafts.js +26 -0
- package/src/excerpts.js +43 -0
- package/src/podcastFeed.njk +85 -0
package/LICENCE.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright 2024 Nathan Bottomley
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# eleventy-plugin-podcaster 🕚⚡️🎈🐀🎤📲
|
|
2
|
+
|
|
3
|
+
`eleventy-plugin-podcaster` — or **Podcaster**, as we will call it from now on — lets you use Eleventy to create a podcast and its accompanying website. **Podcaster** creates the podcast feed that you submit to Apple Podcasts, Spotify or any other podcast directory. And it provides information about your podcast to your Eleventy templates. This means that you can include information about the podcast and its episodes on your podcast's website, creating pages for individual episodes, guests, topics, seasons or anything else at all.
|
|
4
|
+
|
|
5
|
+
Plenty of services exist that will host your podcast online — [Spotify][], [Acast][], [Podbean][], [Buzzsprout][], [Blubrry][]. But none of these will allow you to own your podcast's presence on the web, and none of them will give you the freedom to create a site that presents your podcast in a way that reflects its premise, tone and style.
|
|
6
|
+
|
|
7
|
+
But **Podcaster** will.
|
|
8
|
+
|
|
9
|
+
[Spotify]: https://podcasters.spotify.com
|
|
10
|
+
[Acast]: https://www.acast.com
|
|
11
|
+
[Podbean]: https://www.podbean.com
|
|
12
|
+
[Buzzsprout]: https://www.buzzsprout.com
|
|
13
|
+
[Blubrry]: https://blubrry.com
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
To install the npm package, type this at the command line:
|
|
18
|
+
|
|
19
|
+
```shell
|
|
20
|
+
npm install eleventy-plugin-podcaster
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
And then include the plugin in your Eleventy configuration file.
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// eleventy.config.js
|
|
27
|
+
|
|
28
|
+
import podcaster from 'eleventy-plugin-podcaster'
|
|
29
|
+
|
|
30
|
+
export default function (eleventyConfig) {
|
|
31
|
+
.
|
|
32
|
+
.
|
|
33
|
+
eleventyConfig.addPlugin(podcaster)
|
|
34
|
+
.
|
|
35
|
+
.
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Podcast information
|
|
40
|
+
|
|
41
|
+
Once you've installed **Podcaster** in your Eleventy project, the next step is to provide it with information about your podcast — the title, the owner, the category, the subcategory and so on. The easiest way to do this is to put all the information in your data directory in a `podcast.json` file.
|
|
42
|
+
|
|
43
|
+
Here's an example.
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"title": "Flight Through Entirety: A Doctor Who Podcast",
|
|
48
|
+
"description": "Flying through the entirety of Doctor Who. Originally with cake, but now with guests.",
|
|
49
|
+
"siteUrl": "https://flightthroughentirety.com",
|
|
50
|
+
"author": "Flight Through Entirety",
|
|
51
|
+
"category": "TV & Film",
|
|
52
|
+
"language": "en-AU",
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
[Read more about podcast information.](docs/podcast-information.md)
|
|
57
|
+
|
|
58
|
+
## Episode information
|
|
59
|
+
|
|
60
|
+
For each podcast episode you create, you will also create a Eleventy template containing the information about it — the title, the release date, the episode number, the filename and so on. This template will have the tag `podcastEpisode`; its front matter will contain all of the information about the episode — title, release date, episode number and so on — and its content will contain the episode's show notes.
|
|
61
|
+
|
|
62
|
+
Here's an example.
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
---
|
|
66
|
+
title: Entering a new Phase
|
|
67
|
+
date: 2024-04-14
|
|
68
|
+
tags:
|
|
69
|
+
- podcastEpisode
|
|
70
|
+
episode:
|
|
71
|
+
filename: 500YD S1E1, Entering a New Phase.mp3
|
|
72
|
+
seasonNumber: 1
|
|
73
|
+
episodeNumber: 1
|
|
74
|
+
size: 61231442 # bytes
|
|
75
|
+
duration: 3778.482 # seconds
|
|
76
|
+
---
|
|
77
|
+
A big week for beginnings this week, with a new Doctor, a new origin story for the Daleks, and a whole new approach to defeating the bad guys. Oh, and a new podcast to discuss them all on. So let's welcome Patrick Troughton to the studio floor, as we discuss _The Power of the Daleks_.
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
[Read more about episode information.](docs/episode-information.md)
|
|
82
|
+
|
|
83
|
+
## The podcast feed
|
|
84
|
+
|
|
85
|
+
To create your podcast feed, **Podcaster** needs both the information you've provided about your podcast and the information you've provided about your individual episodes.
|
|
86
|
+
|
|
87
|
+
By default, your podcast feed will be located at `/feed/podcast.xml`, which means that the URL you submit to Apple Podcasts or Spotify (or wherever) will be `{{ podcast.siteUrl }}/feed/podcast.xml`
|
|
88
|
+
|
|
89
|
+
## Using podcast information and episode information in templates
|
|
90
|
+
|
|
91
|
+
All the podcast and episode information you provide are made available to your templates through the data cascade, including `title` and `date`, as well as fields in the `podcast` and `episode` objects.
|
|
92
|
+
|
|
93
|
+
Here's how you could use this information to describe a single podcast episode in a Liquid template.
|
|
94
|
+
|
|
95
|
+
```liquid
|
|
96
|
+
<article>
|
|
97
|
+
<h1>{{ title }}</h1>
|
|
98
|
+
<p class="episode-number">Episode {{ episode.episodeNumber }}</p>
|
|
99
|
+
<p class="release-date">{{ date | date: "%A %-e %B %Y" }}</p>
|
|
100
|
+
<section class="content">
|
|
101
|
+
{{ content }}
|
|
102
|
+
</section>
|
|
103
|
+
<audio controls src="{{ episode.url }}" preload="none">
|
|
104
|
+
<p class="audio-details">
|
|
105
|
+
Episode {{ episode.episodeNumber }}: {{ title }}
|
|
106
|
+
| Recorded on {{ recordingDate | date: "%A %-e %B %Y" }}
|
|
107
|
+
| {{ episode.size | readableSize }}
|
|
108
|
+
| Duration {{ episode.duration | readableDuration }}
|
|
109
|
+
| <a download href="{{ episode.url }}">Download</a>
|
|
110
|
+
</p>
|
|
111
|
+
</article>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
All podcast episode templates belong to the `collections.podcastEpisode` collection, which means you can list several episodes on a single page using [Eleventy's pagination feature][pagination]. In that case, each episode's information will be available in its [collection item data structure][item].
|
|
115
|
+
|
|
116
|
+
[pagination]: https://www.11ty.dev/docs/pagination/
|
|
117
|
+
[item]: https://www.11ty.dev/docs/collections/#collection-item-data-structure
|
|
118
|
+
|
|
119
|
+
> [!TIP]
|
|
120
|
+
> `episode.size` gives you the podcast's size in bytes and `episode.duration`
|
|
121
|
+
> gives you the duration in seconds. To include size and duration
|
|
122
|
+
> in your templates in a human-readable format, use **Podcaster**’s filters
|
|
123
|
+
> `readableSize` and `readableDuration`.
|
|
124
|
+
|
|
125
|
+
## Hosting
|
|
126
|
+
|
|
127
|
+
You can host your podcast site — along with its feed — [the same way you would host any Eleventy site][hosting], using [a Jamstack provider][] linked to your source control repository or using [a classic web host][] which will allow you to upload the contents of your output directory.
|
|
128
|
+
|
|
129
|
+
[hosting]: https://www.11ty.dev/docs/deployment/
|
|
130
|
+
[a Jamstack provider]: https://www.11ty.dev/docs/deployment/#jamstack-providers
|
|
131
|
+
[a classic web host]: https://www.11ty.dev/docs/deployment/#classic-web-hosts
|
|
132
|
+
|
|
133
|
+
However, your podcast episode files should probably be hosted somewhere else, preferably on a Content Delivery Network (CDN), which will let your listeners download your episodes promptly and quickly.
|
|
134
|
+
|
|
135
|
+
There are many options available, including [Digital Ocean Spaces][], [Linode Object Storage][], [Backblaze B2 Cloud Storage][] and [Cloudflare R2][].
|
|
136
|
+
|
|
137
|
+
[Digital Ocean Spaces]: https://www.digitalocean.com/products/spaces
|
|
138
|
+
[Linode Object Storage]: https://www.linode.com/products/object-storage/
|
|
139
|
+
[Backblaze B2 Cloud Storage]: https://www.backblaze.com/cloud-storage
|
|
140
|
+
[Cloudflare R2]: https://developers.cloudflare.com/r2/
|
|
141
|
+
|
|
142
|
+
To find out how to set this up and how to make this work with **Podcaster**, [read more about hosting your podcast episode files][episode-file-hosting].
|
|
143
|
+
|
|
144
|
+
[episode-file-hosting]: docs/hosting.md
|
|
145
|
+
|
|
146
|
+
## Optional features
|
|
147
|
+
|
|
148
|
+
**Podcaster** also implements some optional features which are useful for creating podcast websites — **drafts** and **excerpts**.
|
|
149
|
+
|
|
150
|
+
These are not fundamental features of a podcast website, which is why they are opt-in. You activate them by passing options to the `addPlugin` method in your configuration file.
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
eleventyConfig.addPlugin(podcaster, {
|
|
154
|
+
handleDrafts: true,
|
|
155
|
+
handleExcerpts: true
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
[Read more about optional features.](docs/optional-features.md)
|
|
160
|
+
|
|
161
|
+
## Podcaster in action
|
|
162
|
+
|
|
163
|
+
I started podcasting and creating podcasting websites in 2014. At first I used Squarespace, then WordPress, and then Jekyll, before finally settling on Eleventy late in 2022.
|
|
164
|
+
|
|
165
|
+
I now have seven podcast websites powered by Eleventy, and **Podcaster** was derived from the code I used to create them.
|
|
166
|
+
|
|
167
|
+
Here's a list:
|
|
168
|
+
|
|
169
|
+
- [Flight Through Entirety](https://flightthroughentirety.com), a _Doctor Who_ podcast flying through the entirety of the show's 60-something-year history.
|
|
170
|
+
- [Untitled Star Trek Project](https://untitledstartrekproject.com), a _Star Trek_ commentary podcast, where two friends watch _Star Trek_ episodes from across the franchise, chosen (nearly) at random using [a page on the podcast website](https://untitledstartrekproject.com/randomiser).
|
|
171
|
+
- [500 Year Diary](https://500yeardiary), another _Doctor Who_ podcast, where we look at the show's themes and ideas and some of the people involved in its creation.
|
|
172
|
+
- [The Second Great and Bountiful Human Empire](https://thesecondgreatandbountifulhumanempire.com), a _Doctor Who_ flashcast, where we give our initial reactions to each episode of the post-2023 era of the show.
|
|
173
|
+
- [Startling Barbara Bain](https://startlingbarbarabain), a commentary podcast on _Space: 1999_, a lavish and generally ridiculous British scifi show from the 1970s.
|
|
174
|
+
- [Maximum Power](https://maximumpowerpodcast.com), a podcast about _Blakes 7_, a less lavish but more ridiculous British scifi show from the 1970s.
|
|
175
|
+
- [Bondfinger](https://bondfinger.com), a James Bond commentary podcast that soon ran out of James Bond films and ended up spending its time drinking and watching terrible TV shows from the 1960s.
|
|
176
|
+
|
|
177
|
+
## Licence
|
|
178
|
+
|
|
179
|
+
This plugin is available as open source under the terms of the [ISC License](https://opensource.org/licenses/ISC).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Episode information
|
|
2
|
+
|
|
3
|
+
Each episode of your podcast should have an associated Eleventy template, with a `podcastEpisode` tag. The front matter of this template will contain the necessary information about the episode, and the content of the template will be the show notes.
|
|
4
|
+
|
|
5
|
+
## Front matter
|
|
6
|
+
|
|
7
|
+
The important information about each of your podcast episodes — the title, the date, the filename, the episode number, the size, the duration — should be made available in an `episode` object in the front matter of a post with an `podcastEpisode` tag, like this:
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
---
|
|
11
|
+
title: Entering a new Phase
|
|
12
|
+
date: 2024-04-14
|
|
13
|
+
tags:
|
|
14
|
+
- podcastEpisode
|
|
15
|
+
episode:
|
|
16
|
+
filename: 500YD S1E1, Entering a New Phase.mp3
|
|
17
|
+
seasonNumber: 1
|
|
18
|
+
episodeNumber: 1
|
|
19
|
+
size: 61231442
|
|
20
|
+
duration: 3283
|
|
21
|
+
explicit: no
|
|
22
|
+
episodeType: full
|
|
23
|
+
excerpt: >-
|
|
24
|
+
A big week for beginnings this week, with a new Doctor,
|
|
25
|
+
a new origin story for the Daleks, and a whole new approach
|
|
26
|
+
to defeating the bad guys. Oh, and a new podcast to discuss
|
|
27
|
+
them all on. So let’s welcome Patrick Troughton to the studio
|
|
28
|
+
floor, as we discuss _The Power of the Daleks_.
|
|
29
|
+
---
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Here's a detailed description of the data you need to provide here.
|
|
33
|
+
|
|
34
|
+
| field | value | required? |
|
|
35
|
+
| ----- | ----- | ----- |
|
|
36
|
+
| `title` | The title of the episode; this will also be the title of the post on the website. | yes |
|
|
37
|
+
| `date` | The release date of the episode; this will also be the date of the post on the website | yes |
|
|
38
|
+
| `tags` | Every episode post must have the tag `podcastEpisode` included in the `tags` array. Other tags are also permitted. | yes |
|
|
39
|
+
| `episode.filename` | The filename of the episode's audio file. | yes |
|
|
40
|
+
| `episode.seasonNumber` | The season number. (Most podcasts don't group their episodes into seasons.) | no |
|
|
41
|
+
| `episode.episodeNumber` | The episode number. Needn't be unique, but the combination of `seasonNumber` and `episodeNumber` must be unique. | yes |
|
|
42
|
+
| `episode.size` | The size of the episode's audio file in bytes. | yes |
|
|
43
|
+
| `episode.duration` | The duration of the episode as a number of seconds. You can convert this to `h:mm:ss` format using Podcaster's `readableDuration` filter. | yes |
|
|
44
|
+
| `episode.explicit` | Warns listeners that this episode contains explicit language. Should be used for a single episode in a podcast that isn't itself marked as explicit. | no |
|
|
45
|
+
| `episode.type` | The type of episode. Defaults to `full`, meaning a full episode of the podcast. Other valid types are `trailer` and `bonus`. | no |
|
|
46
|
+
| `excerpt` | A shorter version of the content of the post, written in Markdown. For use in lists of episodes where the show notes are long. For other ways of providing excerpts to **Podcaster**, check out its [optional excerpts feature][excerpts]. | no |
|
|
47
|
+
|
|
48
|
+
[excerpts]: /docs/optional-features.md#excerpts
|
|
49
|
+
|
|
50
|
+
> [!TIP]
|
|
51
|
+
> It's possible for **Podcaster** to calculate the size and duration for each episode if it has access to your episode audio files. [Read more to find out how](docs/size-and-duration.md).
|
|
52
|
+
|
|
53
|
+
> [!TIP]
|
|
54
|
+
> It's also possible for **Podcaster** to automatically create an `excerpt` for each episode. [Read more to find out how](/docs/optional-features.md#excerpts).
|
package/docs/hosting.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Hosting your podcast episode files
|
|
2
|
+
|
|
3
|
+
As it says in [README.md](README.md), you should probably host your podcast files separately from your website. I host my podcast episode files in [Digital Ocean Spaces][], which is inexpensive and not particularly difficult to set up. But there are many other options available, including [Linode Object Storage][], [Backblaze B2 Cloud Storage][] and [Cloudflare R2][].
|
|
4
|
+
|
|
5
|
+
[Digital Ocean Spaces]: https://www.digitalocean.com/products/spaces
|
|
6
|
+
[Linode Object Storage]: https://www.linode.com/products/object-storage/
|
|
7
|
+
[Backblaze B2 Cloud Storage]: https://www.backblaze.com/cloud-storage
|
|
8
|
+
[Cloudflare R2]: https://developers.cloudflare.com/r2/
|
|
9
|
+
|
|
10
|
+
Your CDN host will assign URLs to each of your podcast episodes: you will tell **Podcaster** about these URLs by defining `podcast.episodeUrlBase` as the base URL for all of your podcast episodes, like this: `https://example-podcast.sfo3.digitaloceanspaces.com`. You will provide the filename for each episode as `episode.filename`.
|
|
11
|
+
|
|
12
|
+
To deploy your episodes to your CDN host, you can write an NPM script using [`rclone`][rclone], `rsync` or `s3cmd`, depending on your setup.
|
|
13
|
+
|
|
14
|
+
[rclone]: https://rclone.org
|
|
15
|
+
|
|
16
|
+
Here's the script that I use to deploy the episodes for one of my sites to a DigitalOcean Space.
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
rclone sync episodes/ digitalocean:startlingbarbarabain -P --exclude .DS_Store
|
|
20
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Optional features
|
|
2
|
+
|
|
3
|
+
**Podcaster** includes some optional features which you might find useful for your podcasting website. These features are turned off by default, in case you want to implement them some other way. You can enable one or both of them when you include the plugin in your eleventy configuration file, like this:
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
// eleventy.config.js
|
|
7
|
+
|
|
8
|
+
import podcasterPlugin from 'eleventy-plugin-podcaster'
|
|
9
|
+
|
|
10
|
+
export default function (eleventyConfig) {
|
|
11
|
+
.
|
|
12
|
+
.
|
|
13
|
+
eleventyConfig.addPlugin(podcasterPlugin, {
|
|
14
|
+
handleDrafts: true,
|
|
15
|
+
handleExcerpts: true
|
|
16
|
+
})
|
|
17
|
+
.
|
|
18
|
+
.
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Drafts
|
|
23
|
+
|
|
24
|
+
If `handleDrafts` is set to `true`, the plugin will allow you to designate posts as drafts by including `draft: true` in their front matter (or elsewhere in the data cascade). By default, drafts will be included in the build when Eleventy is running in `serve` or `watch` mode, but will be excluded in `build` mode. You can override this default behaviour by setting the `BUILD_DRAFTS` environment variable to `true` or `false`.
|
|
25
|
+
|
|
26
|
+
## Excerpts
|
|
27
|
+
|
|
28
|
+
If `handleExcerpts` is set to `true`, the plugin will create excerpts for your podcast episode posts. Excerpts are shortened versions of your final content, which you can use on index pages or topic pages or guest pages instead of your complete post.
|
|
29
|
+
|
|
30
|
+
Excerpts are available in a template as `{{ excerpt }}`, but you will probably access them from a collection item, where they are available as `{{ item.data.excerpt }}`. Excerpts are HTML fragments.
|
|
31
|
+
|
|
32
|
+
**Podcaster** defines the excerpt in one of three ways, in order of priority.
|
|
33
|
+
|
|
34
|
+
1. As an `excerpt` field in the post's front matter. This should be written in Markdown.
|
|
35
|
+
2. The part of the post between the excerpt delimiters `<!---excerpt-->` and `<!---endexcerpt-->` .
|
|
36
|
+
3. The first paragraph in the post which is not nested inside another tag. (This is so that a blockquote at the beginning of a post isn't included in the excerpt.)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Podcast information
|
|
2
|
+
|
|
3
|
+
The important information about your podcast — the title, the owner, the category, the subcategory and so on — should be made available as fields in a `podcast` object in the data cascade. The easiest way to do this is to put all the required information in your data directory in a `podcast.json` file, like this:
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{
|
|
7
|
+
"title": "Flight Through Entirety: A Doctor Who Podcast",
|
|
8
|
+
"description": "Flying through the entirety of Doctor Who. Originally with cake, but now with guests.",
|
|
9
|
+
"siteUrl": "https://flightthroughentirety.com",
|
|
10
|
+
"author": "Flight Through Entirety",
|
|
11
|
+
"category": "TV & Film",
|
|
12
|
+
"language": "en-AU",
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
However, `eleventy-plugin-podcast` is quite customisable. He's another `podcast.json` file, with all of the valid fields included.
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"feedPath": "/podcast.xml",
|
|
23
|
+
"title": "Flight Through Entirety: A Doctor Who Podcast",
|
|
24
|
+
"subtitle": "Flying through the entirety of Doctor Who",
|
|
25
|
+
"summary": "Flying through the entirety of Doctor Who. Originally with cake,but now with guests.",
|
|
26
|
+
"description": "Flying through the entirety of Doctor Who. Originally with cake, but now with guests.",
|
|
27
|
+
"siteUrl": "https://flightthroughentirety.com",
|
|
28
|
+
"owner": {
|
|
29
|
+
"name": "Nathan Bottomley",
|
|
30
|
+
"email": "nathan@example.com"
|
|
31
|
+
},
|
|
32
|
+
"author": "Flight Through Entirety",
|
|
33
|
+
"category": "TV & Film",
|
|
34
|
+
"subcategory": "TV Reviews",
|
|
35
|
+
"imagePath": "/assets/images/podcast-logo.jpg",
|
|
36
|
+
"explicit": false,
|
|
37
|
+
"type": "episodic",
|
|
38
|
+
"complete": "no",
|
|
39
|
+
"language": "en-AU",
|
|
40
|
+
"copyright": "Flight Through Entirety",
|
|
41
|
+
"startingYear": 2014,
|
|
42
|
+
"episodeUrlBase": "https://example.fte-cdn.com/",
|
|
43
|
+
"feedEpisodeContentTemplate": "feed-episode-content.njk",
|
|
44
|
+
"feedEpisodeDescriptionTemplate": "feed-episode-description.njk"
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
And here's a detailed description of all of this information.
|
|
49
|
+
|
|
50
|
+
| field | value | required? |
|
|
51
|
+
| ----- | ----- | --------- |
|
|
52
|
+
| `feedPath` | The path where the podcast feed will be located. Defaults to `/feed/podcast.xml`. | no |
|
|
53
|
+
| `title` | The title of your podcast. | yes |
|
|
54
|
+
| `subtitle` | A short description of your podcast. If you omit this the `description` field will be used instead. | no |
|
|
55
|
+
| `summary` | A description of your podcast. If you omit this the `description` field will be used instead. | no |
|
|
56
|
+
| `description` | A short description of your podcast. The most popular podcast applications prominently display this information. | yes |
|
|
57
|
+
| `siteUrl` | The URL of your podcast website. The most popular podcast applications use this to provide a link to your website. It's also used by this plugin to convert relative links to absolute links in your feed. (If `podcast.siteUrl` isn't provided, the feed template will use `site.url` instead.) | yes |
|
|
58
|
+
| `owner` | An optional object in the form `{ name, email }` You might want to omit this: Apple Podcasts has deprecated it, and an email in a podcast feed will attract some spam. However, some podcast directories, like Castbox, will use the email address to identify you when you try to claim ownership of a podcast in their directory. | no |
|
|
59
|
+
| `author` | The creator or creators of the podcast. The most popular podcast applications prominently display this information. | yes |
|
|
60
|
+
| `category` | The category for the podcast. Describes he kind of show it is. Valid categories are listed in [this Apple support document][categories]. Used by podcast directories to help listeners find the podcast. | yes |
|
|
61
|
+
| `subcategory` | The subcategory for the podcast. Valid subcategories are also listed in [the Apple support document][categories]. | no |
|
|
62
|
+
| `imagePath` | The path to your podcast logo, which should be a JPEG or PNG file 3000 × 3000 pixels in size. (You can find more detailed specifications in [this Apple support document](https://podcasters.apple.com/support/896-artwork-requirements#shows)). Defaults to `/img/podcast-logo.jpg`. | yes |
|
|
63
|
+
| `explicit` | Warns listeners that your podcast contains explicit language. In Apple Podcasts, if you include this with the value `true`, your podcast and its episodes will be badged with an 🄴 to indicate that they use explicit language. Some of the most popular podcast applications ignore this field. | no |
|
|
64
|
+
| `type` | Two possible values: `episodic` and `serial`. Defaults to `episodic`, which means that the podcast can be listened to in no particular order. Narrative podcasts (like _Serial_) should be marked as `serial`. | no |
|
|
65
|
+
| `complete` | Indicates that a podcast is complete and that no new episodes should be expected, in which case it should have the value `true`. Should be omitted otherwise. | no |
|
|
66
|
+
| `language` | A code that specifies the language of the feed (rather than the podcast). You can find [a list of permissible codes][lang] at the RSS Advisory Board's website. | yes |
|
|
67
|
+
| `copyright` | The copyright owner of the podcast. If omitted, the value supplied for `author` is used instead. | no |
|
|
68
|
+
| `startingYear` | The year your podcast started. Used to express the copyright date as a range (_"© 2014–2024 Flight Through Entirety"_). If this is omitted, the copyright date will just be the current year. | no |
|
|
69
|
+
| `episodeUrlBase` | If you store your podcast episodes on a CDN, or if you use a podcast analytics service, this is where you specify the base URL for them. If you don't specify this, it defaults to `https://{{ podcast.siteUrl }}/episodes/` | no |
|
|
70
|
+
| `feedEpisodeContentTemplate` | The name of an include template that will be used to create the show notes of each episode, as displayed in your listeners' podcast players. The content of this template should be HTML. You only need to include this if you want the show notes in podcast players to be different from the show notes on the website. | no |
|
|
71
|
+
| `feedEpisodeDescriptionTemplate` | The name of an include template that will be used to create the description of each episode. The content of this template should be plain text. If it's omitted, the description will just be an abbreviated text version of the `content` of the episode's post. | no |
|
|
72
|
+
|
|
73
|
+
[categories]: https://podcasters.apple.com/support/1691-apple-podcasts-categories
|
|
74
|
+
[lang]: https://www.rssboard.org/rss-language-codes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Size and duration
|
|
2
|
+
|
|
3
|
+
The simplest way of telling **Podcaster** the size and duration of your podcast episode files is by including that information in the front matter of each episode's post, like this:
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
episode:
|
|
7
|
+
size: 61231442 # bytes
|
|
8
|
+
duration: 3778.482 # seconds
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
However, you can also get **Podcaster** to analyses your podcast's episode files and find that information for you.
|
|
12
|
+
|
|
13
|
+
To do this, you need a directory which contains all of your podcast's episode files. **Podcaster** assumes that this is an `episodes` folder at the top level of your project. If it finds the folder, it will analyse all of its `.mp3` files, find their size and duration, and save that information in a JSON file called `episodesData.json` in your project's data directory.
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"500YD S1E1, Entering a New Phase (The Power of the Daleks).mp3": {
|
|
18
|
+
"size": 61231442,
|
|
19
|
+
"duration": 3778.482
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
During the build, **Podcaster** will use this JSON file behind the scenes to retrieve `episode.size` and `episode.duration`, both in the podcast feed and in your templates, using the filename you have provided as `episode.filename`.
|
|
25
|
+
|
|
26
|
+
You can specify another folder for **Podcaster** to use when you include **Podcaster** in your configuration file by specifying a relative or absolute path like this.
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import podcasterPlugin from 'eleventy-plugin-podcaster'
|
|
30
|
+
|
|
31
|
+
export default function (eleventyConfig) {
|
|
32
|
+
.
|
|
33
|
+
.
|
|
34
|
+
eleventyConfig.addPlugin(podcasterPlugin, {
|
|
35
|
+
episodesDir: '~/episodes'
|
|
36
|
+
})
|
|
37
|
+
.
|
|
38
|
+
.
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Why `episodesData.json`?
|
|
43
|
+
|
|
44
|
+
When you're building your site locally, you can easily point **Podcaster** at a local directory full of your podcast's episode files and then run your build.
|
|
45
|
+
|
|
46
|
+
But if you host your site on a Jamstack provider and it's building your site for you, it won't have access to a directory like that. (Your podcast files are too big to store in your repository.)
|
|
47
|
+
|
|
48
|
+
So you can run your build locally first, and then commit the resulting `episodesData.json` file to your repository. If you do that, the build process on your host will use the information there to work our your episodes' size and duration.
|
|
49
|
+
|
|
50
|
+
## Skipping the process
|
|
51
|
+
|
|
52
|
+
**Podcaster** will analyse your MP3 files only once during a `--serve` session. To get it to run the analysis again, you need to restart the web server.
|
|
53
|
+
|
|
54
|
+
If you don't want to run the analysis process during a build, you can just set the environment variable `SKIP_EPISODE_CALCULATIONS` to `true`.
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
SKIP_EPISODE_CALCULATIONS=true npx @11ty/eleventy --serve
|
|
58
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
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'
|
|
6
|
+
import calculateSizeAndDuration from './src/calculateSizeAndDuration.js'
|
|
7
|
+
import excerpts from './src/excerpts.js'
|
|
8
|
+
import drafts from './src/drafts.js'
|
|
9
|
+
|
|
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
|
+
}
|
|
14
|
+
|
|
15
|
+
eleventyConfig.addPlugin(rssPlugin, {
|
|
16
|
+
posthtmlRenderOptions: {
|
|
17
|
+
closingSingleTag: 'default' // opt-out of <img/>-style XHTML single tags
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
eleventyConfig.addGlobalData('eleventyComputed.podcast.feedPath', () => {
|
|
22
|
+
return data => data.podcast.feedPath || '/feed/podcast.xml'
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
eleventyConfig.addGlobalData('eleventyComputed.podcast.imagePath', () => {
|
|
26
|
+
return data => data.podcast.imagePath || '/img/podcast-logo.jpg'
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
eleventyConfig.addGlobalData('eleventyComputed.podcast.copyrightNotice', () => {
|
|
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
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
eleventyConfig.addGlobalData(
|
|
43
|
+
'podcast.feedLastBuildDate',
|
|
44
|
+
DateTime.now().toRFC2822()
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
eleventyConfig.addGlobalData('eleventyComputed.episode.url', () => {
|
|
48
|
+
return data => {
|
|
49
|
+
if (!data.tags?.includes('podcastEpisode')) return
|
|
50
|
+
|
|
51
|
+
const episodeUrlBase = data.podcast.episodeUrlBase
|
|
52
|
+
const filename = data.episode.filename
|
|
53
|
+
return new URL(filename, episodeUrlBase).toString()
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
eleventyConfig.addShortcode('year', () => DateTime.now().year)
|
|
58
|
+
|
|
59
|
+
eleventyConfig.addFilter('readableDate', function (date) {
|
|
60
|
+
if (date instanceof Date) {
|
|
61
|
+
date = date.toISOString()
|
|
62
|
+
}
|
|
63
|
+
const result = DateTime.fromISO(date, {
|
|
64
|
+
zone: 'UTC'
|
|
65
|
+
})
|
|
66
|
+
return result.setLocale('en-GB').toLocaleString(DateTime.DATE_HUGE)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
eleventyConfig.addFilter('readableDuration', function (seconds) {
|
|
70
|
+
if (!seconds) return '0:00:00'
|
|
71
|
+
|
|
72
|
+
return Duration.fromMillis(seconds * 1000).toFormat('h:mm:ss')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
eleventyConfig.addFilter('readableSize', bytes =>
|
|
76
|
+
hr.fromBytes(bytes)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const podcastFeedPath = path.join(import.meta.dirname, './src/podcastFeed.njk')
|
|
80
|
+
|
|
81
|
+
eleventyConfig.addTemplate('feed.njk', readFileSync(podcastFeedPath), {
|
|
82
|
+
eleventyExcludeFromCollections: true,
|
|
83
|
+
eleventyImport: {
|
|
84
|
+
collections: ['podcastEpisode']
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
eleventyConfig.addPlugin(calculateSizeAndDuration, options)
|
|
89
|
+
eleventyConfig.addPlugin(excerpts, options)
|
|
90
|
+
eleventyConfig.addPlugin(drafts, options)
|
|
91
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eleventy-plugin-podcaster",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "An Eleventy plugin that allows you to create a podcast and its accompanying website",
|
|
5
|
+
"main": "eleventy.config.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./eleventy.config.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "ava"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"eleventy",
|
|
15
|
+
"eleventy-plugin",
|
|
16
|
+
"podcast",
|
|
17
|
+
"podcasting",
|
|
18
|
+
"rss"
|
|
19
|
+
],
|
|
20
|
+
"author": "Nathan Bottomley",
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@11ty/eleventy": "^3.0.0",
|
|
24
|
+
"@11ty/eleventy-plugin-rss": "^2.0.1",
|
|
25
|
+
"@tsmx/human-readable": "^2.0.3",
|
|
26
|
+
"chalk": "^5.3.0",
|
|
27
|
+
"dom-serializer": "^2.0.0",
|
|
28
|
+
"htmlparser2": "^9.1.0",
|
|
29
|
+
"luxon": "^3.4.4",
|
|
30
|
+
"markdown-it": "^14.1.0",
|
|
31
|
+
"mp3-duration": "^1.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"ava": "^6.1.3",
|
|
35
|
+
"fast-xml-parser": "^4.4.0",
|
|
36
|
+
"neostandard": "^0.11.4"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Duration } from 'luxon'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { readdir, stat, readFile, writeFile } from 'node:fs/promises'
|
|
5
|
+
import mp3Duration from 'mp3-duration'
|
|
6
|
+
import hr from '@tsmx/human-readable'
|
|
7
|
+
import chalk from 'chalk'
|
|
8
|
+
|
|
9
|
+
const convertSecondsToReadableDuration = seconds =>
|
|
10
|
+
Duration.fromMillis(seconds * 1000)
|
|
11
|
+
.toFormat("d 'd' h 'h' m 'm' s.SSS 's'")
|
|
12
|
+
|
|
13
|
+
export default function (eleventyConfig, options = {}) {
|
|
14
|
+
let firstRun = true
|
|
15
|
+
eleventyConfig.on('eleventy.before', async ({ dir, directories }) => {
|
|
16
|
+
// don't keep recalculating episode data in serve mode
|
|
17
|
+
if (!firstRun || process.env.SKIP_EPISODE_CALCULATIONS === 'true') return
|
|
18
|
+
firstRun = false
|
|
19
|
+
|
|
20
|
+
const episodesDir = path.join(process.cwd(), options.episodesDir || 'episodes')
|
|
21
|
+
if (!existsSync(episodesDir)) return
|
|
22
|
+
|
|
23
|
+
const episodes = await readdir(episodesDir)
|
|
24
|
+
const episodesData = {}
|
|
25
|
+
let totalEpisodes = 0
|
|
26
|
+
let totalSize = 0
|
|
27
|
+
let totalDuration = 0
|
|
28
|
+
|
|
29
|
+
for (const episode of episodes) {
|
|
30
|
+
if (!episode.endsWith('.mp3')) continue
|
|
31
|
+
|
|
32
|
+
totalEpisodes++
|
|
33
|
+
const episodePath = path.join(episodesDir, episode)
|
|
34
|
+
const episodeSize = (await stat(episodePath)).size
|
|
35
|
+
totalSize += episodeSize
|
|
36
|
+
const buffer = await readFile(episodePath)
|
|
37
|
+
const episodeDuration = await mp3Duration(buffer)
|
|
38
|
+
totalDuration += episodeDuration
|
|
39
|
+
episodesData[episode] = {
|
|
40
|
+
size: episodeSize,
|
|
41
|
+
duration: episodeDuration
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const dataDir = path.join(process.cwd(), directories.data)
|
|
46
|
+
await writeFile(path.join(dataDir, 'episodesData.json'), JSON.stringify(episodesData, null, 2))
|
|
47
|
+
|
|
48
|
+
console.log(chalk.yellow(`${totalEpisodes} episodes; ${hr.fromBytes(totalSize)}; ${convertSecondsToReadableDuration(totalDuration)}.`))
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
eleventyConfig.addGlobalData('eleventyComputed.episode.size', () => {
|
|
52
|
+
return data => {
|
|
53
|
+
if (data.episode.size) return data.episode.size
|
|
54
|
+
|
|
55
|
+
if (data.tags?.includes('podcastEpisode') && data.episodesData) {
|
|
56
|
+
return data.episodesData[data.episode.filename]?.size
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
eleventyConfig.addGlobalData('eleventyComputed.episode.duration', () => {
|
|
62
|
+
return data => {
|
|
63
|
+
if (data.episode.duration) return data.episode.duration
|
|
64
|
+
|
|
65
|
+
if (data.tags?.includes('podcastEpisode') && data.episodesData) {
|
|
66
|
+
return data.episodesData[data.episode.filename]?.duration
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
package/src/drafts.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export default (eleventyConfig, options = {}) => {
|
|
2
|
+
if (!options.handleDrafts) return
|
|
3
|
+
|
|
4
|
+
let hasLoggedAboutDrafts = false
|
|
5
|
+
eleventyConfig.addPreprocessor('drafts', 'md', (data, _content) => {
|
|
6
|
+
let shouldIncludeDrafts = false
|
|
7
|
+
if (process.env.BUILD_DRAFTS === 'true') {
|
|
8
|
+
shouldIncludeDrafts = true
|
|
9
|
+
} else if (process.env.BUILD_DRAFTS === 'false') {
|
|
10
|
+
shouldIncludeDrafts = false
|
|
11
|
+
} else {
|
|
12
|
+
shouldIncludeDrafts = (process.env.ELEVENTY_RUN_MODE !== 'build')
|
|
13
|
+
}
|
|
14
|
+
if (!hasLoggedAboutDrafts) {
|
|
15
|
+
if (shouldIncludeDrafts) {
|
|
16
|
+
console.log('Including drafts.')
|
|
17
|
+
} else {
|
|
18
|
+
console.log('Excluding drafts.')
|
|
19
|
+
}
|
|
20
|
+
hasLoggedAboutDrafts = true
|
|
21
|
+
}
|
|
22
|
+
if (data.draft && !shouldIncludeDrafts) {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
}
|
package/src/excerpts.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as htmlparser2 from 'htmlparser2'
|
|
2
|
+
import render from 'dom-serializer'
|
|
3
|
+
import markdownIt from 'markdown-it'
|
|
4
|
+
|
|
5
|
+
export default function (eleventyConfig, options = {}) {
|
|
6
|
+
eleventyConfig.addGlobalData('eleventyComputed.excerpt', () => {
|
|
7
|
+
if (!options.handleExcerpts) return
|
|
8
|
+
|
|
9
|
+
return (data) => {
|
|
10
|
+
if (!data.tags?.includes('podcastEpisode')) return
|
|
11
|
+
|
|
12
|
+
const md = markdownIt()
|
|
13
|
+
|
|
14
|
+
// If an excerpt is set in front matter, use it
|
|
15
|
+
if (data.excerpt) {
|
|
16
|
+
return md.render(data.excerpt)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const content = data.page.rawInput
|
|
20
|
+
const contentIsMarkdown = data.page.templateSyntax.includes('md')
|
|
21
|
+
|
|
22
|
+
// If an excerpt is set using comment delimiters, use it
|
|
23
|
+
const excerptPattern = /<!---excerpt-->\s*(.*?)\s*<!---endexcerpt-->/
|
|
24
|
+
const match = excerptPattern.exec(content)
|
|
25
|
+
if (match && contentIsMarkdown) {
|
|
26
|
+
return md.render(match[1])
|
|
27
|
+
} else if (match) {
|
|
28
|
+
return match[1]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// If an excerpt is not set, use the first paragraph not in a blockquote
|
|
32
|
+
let htmlContent
|
|
33
|
+
if (data.page.templateSyntax.includes('md')) {
|
|
34
|
+
htmlContent = md.render(content)
|
|
35
|
+
} // otherwise it's already HTML
|
|
36
|
+
const dom = htmlparser2.parseDocument(htmlContent)
|
|
37
|
+
const paragraph = dom.children.find(item => item.type === 'tag' && item.name === 'p')
|
|
38
|
+
if (paragraph) {
|
|
39
|
+
return render(paragraph)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
permalink: "{{ podcast.feedPath or '/feed/podcast.xml' }}"
|
|
3
|
+
eleventyAllowMissingExtension: true
|
|
4
|
+
---
|
|
5
|
+
{%- set siteUrl %}{{ podcast.siteUrl or site.url }}{% endset -%}
|
|
6
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
7
|
+
<rss version="2.0"
|
|
8
|
+
xmlns:atom="http://www.w3.org/2005/Atom"
|
|
9
|
+
xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
|
|
10
|
+
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
|
11
|
+
>
|
|
12
|
+
<channel>
|
|
13
|
+
<title>{{ podcast.title }}</title>
|
|
14
|
+
<itunes:subtitle>{{ podcast.subtitle or podcast.description }}</itunes:subtitle>
|
|
15
|
+
<description>{{ podcast.description }}</description>
|
|
16
|
+
<link>{{ siteUrl }}</link>
|
|
17
|
+
<atom:link href="{{ podcast.feedPath | htmlBaseUrl(siteUrl) }}" rel="self" type="application/rss+xml" />
|
|
18
|
+
{%- if podcast.owner %}
|
|
19
|
+
<itunes:owner>
|
|
20
|
+
<itunes:name>{{ podcast.owner.name }}</itunes:name>
|
|
21
|
+
<itunes:email>{{ podcast.owner.email }}</itunes:email>
|
|
22
|
+
</itunes:owner>
|
|
23
|
+
{%- endif %}
|
|
24
|
+
<itunes:author>{{ podcast.author }}</itunes:author>
|
|
25
|
+
{%- if podcast.subcategory %}
|
|
26
|
+
<itunes:category text="{{ podcast.category }}">
|
|
27
|
+
<itunes:category text="{{ podcast.subcategory }}" />
|
|
28
|
+
</itunes:category>
|
|
29
|
+
{%- else %}
|
|
30
|
+
<itunes:category text="{{ podcast.category }}" />
|
|
31
|
+
{%- endif %}
|
|
32
|
+
<itunes:image href="{{ podcast.imagePath | htmlBaseUrl(podcast.siteUrl) }}"></itunes:image>
|
|
33
|
+
<itunes:summary>{{ podcast.summary or podcast.description }}</itunes:summary>
|
|
34
|
+
{% if podcast.explicit !== undefined %}<itunes:explicit>{{ podcast.explicit or "false" }}</itunes:explicit>{% endif -%}
|
|
35
|
+
{%- if podcast.type %}
|
|
36
|
+
<itunes:type>{{ podcast.type }}</itunes:type>
|
|
37
|
+
{%- endif %}
|
|
38
|
+
{%- if podcast.complete %}
|
|
39
|
+
<itunes:complete>yes</itunes:complete>
|
|
40
|
+
{%- endif %}
|
|
41
|
+
<language>{{ podcast.language }}</language>
|
|
42
|
+
<copyright>{{ podcast.copyrightNotice }}</copyright>
|
|
43
|
+
<pubDate>{{ collections.podcastEpisode | getNewestCollectionItemDate | dateToRfc3339 }}</pubDate>
|
|
44
|
+
<lastBuildDate>{{ podcast.feedLastBuildDate }}</lastBuildDate>
|
|
45
|
+
<generator>{{ eleventy.generator }}</generator>
|
|
46
|
+
{% for post in collections.podcastEpisode | reverse %}
|
|
47
|
+
<item>
|
|
48
|
+
<title>{{ post.data.title }}</title>
|
|
49
|
+
<link>{{ post.url | htmlBaseUrl(podcast.siteUrl) }}</link>
|
|
50
|
+
<pubDate>{{ post.date | dateToRfc3339 }}</pubDate>
|
|
51
|
+
{% if post.data.episode.seasonNumber -%}
|
|
52
|
+
<itunes:season>{{ post.data.episode.seasonNumber }}</itunes:season>
|
|
53
|
+
{%- endif %}
|
|
54
|
+
<itunes:episode>{{ post.data.episode.episodeNumber }}</itunes:episode>
|
|
55
|
+
<itunes:summary>{{ post.content | striptags(true) | truncate(800) }}</itunes:summary>
|
|
56
|
+
<description>{{ post.content | striptags(true) | truncate(800) }}</description>
|
|
57
|
+
{% if podcast.feedEpisodeContentTemplate %}
|
|
58
|
+
{%- set episodeContent -%}
|
|
59
|
+
{% include podcast.feedEpisodeContentTemplate %}
|
|
60
|
+
{%- endset -%}
|
|
61
|
+
<content:encoded>
|
|
62
|
+
<![CDATA[{{ episodeContent | renderTransforms(post.data.page, podcast.siteUrl) | safe | trim }}]]>
|
|
63
|
+
</content:encoded>
|
|
64
|
+
{%- else -%}
|
|
65
|
+
<content:encoded>
|
|
66
|
+
<![CDATA[{{ post.content | renderTransforms(post.data.page, podcast.siteUrl) | safe | trim }}]]>
|
|
67
|
+
</content:encoded>
|
|
68
|
+
{% endif %}
|
|
69
|
+
<enclosure url="{{ post.data.episode.url }}" length="{{ post.data.episode.size }}" type="audio/mp3"></enclosure>
|
|
70
|
+
<itunes:duration>{{ post.data.episode.duration | readableDuration }}</itunes:duration>
|
|
71
|
+
{%- if post.data.guid != undefined %}
|
|
72
|
+
<guid isPermalink="false">{{ post.data.episode.guid }}</guid>
|
|
73
|
+
{% else %}
|
|
74
|
+
<guid isPermalink="true">{{ post.url | htmlBaseUrl(podcast.siteUrl) }}</guid>
|
|
75
|
+
{% endif -%}
|
|
76
|
+
{%- if post.data.explicit != undefined %}
|
|
77
|
+
<itunes:explicit>{{ post.data.explicit }}</itunes:explicit>
|
|
78
|
+
{% endif -%}
|
|
79
|
+
{%- if post.data.type != undefined %}
|
|
80
|
+
<itunes:episodeType>{{ post.data.episodeType }}</itunes:episodeType>
|
|
81
|
+
{% endif -%}
|
|
82
|
+
</item>
|
|
83
|
+
{% endfor -%}
|
|
84
|
+
</channel>
|
|
85
|
+
</rss>
|