feat: enhance podcast functionality and build pipeline
- Dynamically resolve image paths for podcast episodes in `episodes.11tydata.js` - Add podcast metadata handling with inferred URLs for MP3 and transcripts - Introduce search functionality with Lunr.js and a search index generator - Update Eleventy path prefix handling to support environment variable override - Add `.mp4` files to `.gitignore` - Expand VSCode settings to include Markdown-Eleventy support and improved terminal history - Add deployment script (`deploy.sh`) with remote rsync-based deployment and permission handling - Adjust episode layout to use dynamic image paths and updated podcast metadata - Add search and members page updates, including new URLs and search integration - Update dependencies to include `html-to-text` and related packages for search indexing
This commit is contained in:
@@ -1,17 +1,83 @@
|
||||
const {seasonEpisodeFormat} = require('../../utils/filters');
|
||||
const {seasonEpisodeFormat, episodeNumber} = require('../../utils/filters');
|
||||
const fs = require('fs/promises');
|
||||
path = require('path')
|
||||
|
||||
module.exports = {
|
||||
"layout": "episode",
|
||||
"tags":['episode'],
|
||||
"eleventyComputed": {
|
||||
"episode": "{{page.fileSlug | episodeNumber: episode}}",
|
||||
"image": "{{page.url}}/../image.jpg",
|
||||
"podcast": (data) => {
|
||||
return {
|
||||
"enclosureUrl": data.podcast.enclosureUrl ||`${data.site.cdn}/${seasonEpisodeFormat(null, data).toLowerCase()}.mp3`,
|
||||
"transcriptUrl": data.podcast.transcriptUrl ||`${data.site.cdn}/${seasonEpisodeFormat(null, data).toLowerCase()}.srt`,
|
||||
"title": data.podcast.title || `${seasonEpisodeFormat(null, data)}: ${data.title || "Episode " + data.episode}`,
|
||||
"image" : data.podcast.image || data.image || "{{page.url}}/../image.jpg"
|
||||
}}
|
||||
"image": async (data) => {
|
||||
const image_locations_promises = [
|
||||
data.image,
|
||||
`${data.page.filePathStem}.jpg`,
|
||||
`${data.page.filePathStem}.png`,
|
||||
'../image.jpg',
|
||||
'../image.png',
|
||||
'../../image.jpg',
|
||||
'../../image.png'
|
||||
].filter(image_path=>image_path).map((image_path)=>
|
||||
fs.access(path.resolve(data.page.inputPath, image_path))
|
||||
.then(()=>path.resolve(data.page.filePathStem, image_path))
|
||||
.catch(()=>null)
|
||||
)
|
||||
return (await Promise.all(image_locations_promises)).find(i=>i)
|
||||
},
|
||||
"podcast": podcastData
|
||||
}
|
||||
}
|
||||
|
||||
async function podcastData (data) {
|
||||
var file_stem
|
||||
if (data.season === 1) {
|
||||
file_stem = `ep${data.episode}`
|
||||
} else {
|
||||
file_stem = `${seasonEpisodeFormat(null, data).toLowerCase()}`
|
||||
}
|
||||
const mp3_exists_promise = new Promise ((resolve, reject) => {
|
||||
if (data.podcast.enclosureUrl) {
|
||||
resolve(data.podcast.enclosureUrl)
|
||||
} else {
|
||||
const url = `${data.site.cdn}/${file_stem}.mp3`
|
||||
console.log(`Inferring URL @ ${url} for ${data.page.url}`)
|
||||
fetch(url, { method: "HEAD" })
|
||||
.then((res)=>{
|
||||
if (res.ok) {
|
||||
resolve(`${url}`)
|
||||
} else {
|
||||
reject(new Error(`No file at ${url}`));
|
||||
}
|
||||
})
|
||||
}
|
||||
}).catch((e)=>{return null})
|
||||
|
||||
const transcript_exists_promise = new Promise ((resolve, reject) => {
|
||||
if (data.podcast.transcriptUrl) {
|
||||
resolve(data.podcast.transcriptUrl)
|
||||
} else {
|
||||
const url = `${data.site.cdn}/${file_stem}.srt`
|
||||
console.log(`Inferring URL @ ${url}`)
|
||||
fetch(url, { method: "HEAD" })
|
||||
.then((res)=>{
|
||||
if (res.ok) {
|
||||
resolve(`${url}`)
|
||||
} else {
|
||||
reject(new Error(`No file at ${url}`));
|
||||
}
|
||||
})
|
||||
}
|
||||
}).catch((e)=>{return null})
|
||||
|
||||
const result = {
|
||||
"enclosureUrl": await mp3_exists_promise,
|
||||
"transcriptUrl": await transcript_exists_promise,
|
||||
"title": data.podcast.title || `${seasonEpisodeFormat(null, data)}: ${data.title || "Episode " + data.episode}`,
|
||||
"image" : data.podcast.image || data.image
|
||||
}
|
||||
|
||||
if (result.enclosureUrl != null) {
|
||||
return result
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
const podcastData = require('../episodes.11tydata').eleventyComputed.podcast;
|
||||
|
||||
module.exports = {
|
||||
"season": 1,
|
||||
@@ -6,8 +7,9 @@ module.exports = {
|
||||
],
|
||||
"stylesheets": ["/css/s04.css"],
|
||||
"eleventyComputed": {
|
||||
"podcast": (data)=>({
|
||||
"title": `E${data.episode}: ${data.title}`
|
||||
})
|
||||
"podcast": async (data)=>{
|
||||
var result = {...await podcastData(data),"title": `E${data.episode}: ${data.title}`}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
content/episodes/s02/image.jpg
Executable file
BIN
content/episodes/s02/image.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 290 KiB |
23
content/episodes/search-index.11ty.js
Normal file
23
content/episodes/search-index.11ty.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import markdownit from 'markdown-it';
|
||||
import { convert } from 'html-to-text';
|
||||
const md = markdownit({html: true})
|
||||
|
||||
class SearchIndex {
|
||||
data() {
|
||||
return {eleventyExcludeFromCollections:["episode"], layout: null}
|
||||
}
|
||||
|
||||
render (data) {
|
||||
const documents = data.collections.episode.map((episode)=>{
|
||||
return {
|
||||
url:`${this.url(episode.url)}`,
|
||||
title: episode.data.title,
|
||||
text: convert (episode.content),
|
||||
season: episode.data.season,
|
||||
episode: episode.data.episode
|
||||
}})
|
||||
return JSON.stringify(documents);
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchIndex
|
||||
84
content/episodes/search.hbs
Normal file
84
content/episodes/search.hbs
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
layout: base-with-heading
|
||||
title: Search Episodes
|
||||
eleventyExcludeFromCollections: ["episode"]
|
||||
override:tags: []
|
||||
override:eleventyComputed: []
|
||||
---
|
||||
|
||||
<script src="https://unpkg.com/lunr/lunr.js"></script>
|
||||
|
||||
<div>
|
||||
<form id="search-form">
|
||||
<input type="text" class="form-control mb-3" name="searchQuery" id="searchQuery">
|
||||
<button class="btn btn-primary mb-3" type="submit">Search</button>
|
||||
</form>
|
||||
<div id="results">
|
||||
<ol></ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let idx, docs
|
||||
let search_index_promise = fetch('../search-index')
|
||||
.then((res)=>res.json())
|
||||
.then((documents)=>{
|
||||
docs = documents
|
||||
idx = lunr(function(){
|
||||
this.ref('id')
|
||||
this.field('text')
|
||||
this.field('title')
|
||||
this.metadataWhitelist = ['position']
|
||||
documents.forEach(function (doc, idx) {
|
||||
doc.id = idx;
|
||||
this.add(doc);
|
||||
}, this)
|
||||
})
|
||||
})
|
||||
|
||||
function highlightTokens(element, tokens) {
|
||||
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, nodeFilter, false);
|
||||
let node = null;
|
||||
|
||||
while ((node = walker.nextNode())) {
|
||||
const text = node.textContent.toLowerCase();
|
||||
let found = false;
|
||||
for (var i = 0; i < tokens.length && !found; i++) {
|
||||
const token = tokens[i].toString();
|
||||
const startIndex = text.indexOf(token);
|
||||
if (startIndex == -1) {
|
||||
continue;
|
||||
}
|
||||
let range = document.createRange();
|
||||
range.setStart(node, startIndex);
|
||||
range.setEnd(node, startIndex + token.length);
|
||||
let mark = document.createElement('mark');
|
||||
range.surroundContents(mark);
|
||||
found = true;
|
||||
}
|
||||
walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(evt) {
|
||||
evt.preventDefault();
|
||||
const formData = new FormData(evt.target)
|
||||
const {searchQuery} = Object.fromEntries(formData)
|
||||
results = idx.search(searchQuery)
|
||||
results.forEach(r => {
|
||||
r.title = docs[r.ref].title,
|
||||
r.url = docs[r.ref].url
|
||||
})
|
||||
console.log('Form submitted!', results)
|
||||
results_ol = document.getElementById("results").querySelector('ol')
|
||||
results_ol.innerHTML = ""
|
||||
results.forEach(r => {
|
||||
const el = document.createElement('li')
|
||||
const {url, title, text, season, episode} = docs[r.ref]
|
||||
el.innerHTML =`<a href="${url}">${title} (Season ${season}, episode ${episode})</a><p>${text}</p>`
|
||||
results_ol.appendChild(el)
|
||||
})
|
||||
}
|
||||
document.getElementById('search-form').addEventListener('submit', handleSubmit)
|
||||
|
||||
</script>
|
||||
@@ -3,7 +3,7 @@ layout: base-with-heading
|
||||
title: Member Content
|
||||
links:
|
||||
- name: Foundry
|
||||
url: /foundry
|
||||
url: /foundry/game
|
||||
image: /images/fvtt-solid-512.png
|
||||
- name: Discord
|
||||
url: discord
|
||||
|
||||
Reference in New Issue
Block a user