mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-02-04 10:54:44 -05:00
@@ -5,7 +5,6 @@
|
|||||||
"Read(./secrets/**)",
|
"Read(./secrets/**)",
|
||||||
"Read(./config/credentials.json)",
|
"Read(./config/credentials.json)",
|
||||||
"Read(./build)",
|
"Read(./build)",
|
||||||
"Read(./**.xml)",
|
|
||||||
"Read(./embeddings**)"
|
"Read(./embeddings**)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
DATABASE_URL=example.db
|
DATABASE_URL=example.db
|
||||||
|
PUBLIC_SITE_URL=https://bibdle.com
|
||||||
|
|
||||||
# nodemailer
|
# nodemailer
|
||||||
SMTP_USERNAME=email@example.com
|
SMTP_USERNAME=email@example.com
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -28,4 +28,5 @@ vite.config.ts.timestamp-*
|
|||||||
llms-*
|
llms-*
|
||||||
|
|
||||||
embeddings*
|
embeddings*
|
||||||
*.xml
|
*bible.xml
|
||||||
|
engwebu_usfx.xml
|
||||||
|
|||||||
@@ -24143,7 +24143,7 @@
|
|||||||
<verse number="16">Gather the people, Sanctify the congregation, Assemble the elders, Gather the children and nursing babes; Let the bridegroom go out from his chamber, And the bride from her dressing room.</verse>
|
<verse number="16">Gather the people, Sanctify the congregation, Assemble the elders, Gather the children and nursing babes; Let the bridegroom go out from his chamber, And the bride from her dressing room.</verse>
|
||||||
<verse number="17">Let the priests, who minister to the Lord, Weep between the porch and the altar; Let them say, “Spare Your people, O Lord, And do not give Your heritage to reproach, That the nations should rule over them. Why should they say among the peoples, ‘Where is their God?’ ”</verse>
|
<verse number="17">Let the priests, who minister to the Lord, Weep between the porch and the altar; Let them say, “Spare Your people, O Lord, And do not give Your heritage to reproach, That the nations should rule over them. Why should they say among the peoples, ‘Where is their God?’ ”</verse>
|
||||||
<verse number="18">Then the Lord will be zealous for His land, And pity His people.</verse>
|
<verse number="18">Then the Lord will be zealous for His land, And pity His people.</verse>
|
||||||
<verse number="19">The Lord will answer and say to His people, “Behold, I will send you grain and new wine and oil, And you will be satisfied by them; I will no longer make you a reproach among the nations.</verse>
|
<verse number="19">The Lord will answer and say to His people, “Behold, I will send you grain and new wine and oil, And you will be satisfied by them; I will no longer make you a reproach among the nations.”</verse>
|
||||||
<verse number="20">“But I will remove far from you the northern army, And will drive him away into a barren and desolate land, With his face toward the eastern sea And his back toward the western sea; His stench will come up, And his foul odor will rise, Because he has done monstrous things.”</verse>
|
<verse number="20">“But I will remove far from you the northern army, And will drive him away into a barren and desolate land, With his face toward the eastern sea And his back toward the western sea; His stench will come up, And his foul odor will rise, Because he has done monstrous things.”</verse>
|
||||||
<verse number="21">Fear not, O land; Be glad and rejoice, For the Lord has done marvelous things!</verse>
|
<verse number="21">Fear not, O land; Be glad and rejoice, For the Lord has done marvelous things!</verse>
|
||||||
<verse number="22">Do not be afraid, you beasts of the field; For the open pastures are springing up, And the tree bears its fruit; The fig tree and the vine yield their strength.</verse>
|
<verse number="22">Do not be afraid, you beasts of the field; For the open pastures are springing up, And the tree bears its fruit; The fig tree and the vine yield their strength.</verse>
|
||||||
|
|||||||
@@ -1,241 +1,246 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
interface ImposterData {
|
interface ImposterData {
|
||||||
verses: string[];
|
verses: string[];
|
||||||
refs: string[];
|
refs: string[];
|
||||||
imposterIndex: number;
|
imposterIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: ImposterData | null = null;
|
let data: ImposterData | null = null;
|
||||||
let clicked: boolean[] = [];
|
let clicked: boolean[] = [];
|
||||||
let gameOver = false;
|
let gameOver = false;
|
||||||
let loading = true;
|
let loading = true;
|
||||||
let error: string | null = null;
|
let error: string | null = null;
|
||||||
|
|
||||||
async function loadGame() {
|
async function loadGame() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/imposter");
|
const res = await fetch("/api/imposter");
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||||
}
|
}
|
||||||
data = (await res.json()) as ImposterData;
|
data = (await res.json()) as ImposterData;
|
||||||
clicked = new Array(data.verses.length).fill(false);
|
clicked = new Array(data.verses.length).fill(false);
|
||||||
gameOver = false;
|
gameOver = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : "Unknown error";
|
error = e instanceof Error ? e.message : "Unknown error";
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClick(index: number) {
|
function handleClick(index: number) {
|
||||||
if (gameOver || !data || clicked[index]) return;
|
if (gameOver || !data || clicked[index]) return;
|
||||||
clicked[index] = true;
|
clicked[index] = true;
|
||||||
if (index !== data.imposterIndex) {
|
if (index !== data.imposterIndex) {
|
||||||
clicked[data.imposterIndex] = true;
|
clicked[data.imposterIndex] = true;
|
||||||
}
|
}
|
||||||
gameOver = true;
|
gameOver = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function newGame() {
|
function newGame() {
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
data = null;
|
data = null;
|
||||||
loadGame();
|
loadGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(loadGame);
|
onMount(loadGame);
|
||||||
|
|
||||||
function formatVerse(verse: string): string {
|
function formatVerse(verse: string): string {
|
||||||
let formatted = verse;
|
let formatted = verse;
|
||||||
|
|
||||||
// Handle unbalanced opening/closing punctuation
|
// Handle unbalanced opening/closing punctuation
|
||||||
const pairs: [string, string][] = [
|
const pairs: [string, string][] = [
|
||||||
["(", ")"],
|
["(", ")"],
|
||||||
["[", "]"],
|
["[", "]"],
|
||||||
["{", "}"],
|
["{", "}"],
|
||||||
['"', '"'],
|
['"', '"'],
|
||||||
["'", "'"],
|
["'", "'"],
|
||||||
["\u201C", "\u201D"], // \u201C
|
["\u201C", "\u201D"], // \u201C
|
||||||
["\u2018", "\u2019"], // \u2018
|
["\u2018", "\u2019"], // \u2018
|
||||||
];
|
];
|
||||||
for (const [open, close] of pairs) {
|
for (const [open, close] of pairs) {
|
||||||
if (formatted.startsWith(open) && !formatted.includes(close)) {
|
if (formatted.startsWith(open) && !formatted.includes(close)) {
|
||||||
formatted += "..." + close;
|
formatted += "..." + close;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const [open, close] of pairs) {
|
for (const [open, close] of pairs) {
|
||||||
if (formatted.endsWith(close) && !formatted.includes(open)) {
|
if (formatted.endsWith(close) && !formatted.includes(open)) {
|
||||||
formatted = open + "..." + formatted;
|
formatted = open + "..." + formatted;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/^[a-z]/.test(formatted)) {
|
if (/^[a-z]/.test(formatted)) {
|
||||||
formatted = "..." + formatted;
|
formatted = "..." + formatted;
|
||||||
}
|
}
|
||||||
formatted = formatted.replace(/[,:;-—]$/, "...");
|
// Replace trailing punctuation with ellipsis
|
||||||
return formatted;
|
// Preserve closing quotes/brackets that may have been added
|
||||||
}
|
formatted = formatted.replace(
|
||||||
|
/[,:;-—]([)\]}\"\'\u201D\u2019]*)$/,
|
||||||
|
"...$1",
|
||||||
|
);
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="imposter-game">
|
<div class="imposter-game">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="loading">Loading verses...</p>
|
<p class="loading">Loading verses...</p>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<div class="error">
|
<div class="error">
|
||||||
<p>Error: {error}</p>
|
<p>Error: {error}</p>
|
||||||
<button on:click={newGame}>Retry</button>
|
<button on:click={newGame}>Retry</button>
|
||||||
</div>
|
</div>
|
||||||
{:else if data}
|
{:else if data}
|
||||||
<!-- <div class="instructions">
|
<!-- <div class="instructions">
|
||||||
<p>Click the verse that doesn't belong (from a different book).</p>
|
<p>Click the verse that doesn't belong (from a different book).</p>
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="verses">
|
<div class="verses">
|
||||||
{#each data.verses as verse, i}
|
{#each data.verses as verse, i}
|
||||||
<div class="verse-item">
|
<div class="verse-item">
|
||||||
<button
|
<button
|
||||||
class="verse-button"
|
class="verse-button"
|
||||||
class:clicked={clicked[i]}
|
class:clicked={clicked[i]}
|
||||||
class:correct={clicked[i] && i === data.imposterIndex}
|
class:correct={clicked[i] && i === data.imposterIndex}
|
||||||
class:wrong={clicked[i] && i !== data.imposterIndex}
|
class:wrong={clicked[i] && i !== data.imposterIndex}
|
||||||
on:click={() => handleClick(i)}
|
on:click={() => handleClick(i)}
|
||||||
disabled={gameOver}
|
disabled={gameOver}
|
||||||
>
|
>
|
||||||
{formatVerse(verse)}
|
{formatVerse(verse)}
|
||||||
</button>
|
</button>
|
||||||
{#if gameOver}
|
{#if gameOver}
|
||||||
<div class="ref">{data.refs[i]}</div>
|
<div class="ref">{data.refs[i]}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if gameOver}
|
{#if gameOver}
|
||||||
<div class="result">
|
<div class="result">
|
||||||
<button on:click={newGame}>New Game</button>
|
<button on:click={newGame}>New Game</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.imposter-game {
|
.imposter-game {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading,
|
.loading,
|
||||||
.error {
|
.error {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.instructions {
|
/*.instructions {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
.verses {
|
.verses {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.verse-item {
|
.verse-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.verse-button {
|
.verse-button {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
border: 3px solid #ddd;
|
border: 3px solid #ddd;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.verse-button:hover:not(.clicked):not(:disabled) {
|
.verse-button:hover:not(.clicked):not(:disabled) {
|
||||||
border-color: #007bff;
|
border-color: #007bff;
|
||||||
background: #f8f9ff;
|
background: #f8f9ff;
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.15);
|
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.verse-button:disabled {
|
.verse-button:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.verse-button.clicked {
|
.verse-button.clicked {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.correct {
|
.correct {
|
||||||
background: #d4edda !important;
|
background: #d4edda !important;
|
||||||
border-color: #28a745 !important;
|
border-color: #28a745 !important;
|
||||||
color: #155724;
|
color: #155724;
|
||||||
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.3);
|
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrong {
|
.wrong {
|
||||||
background: #f8d7da !important;
|
background: #f8d7da !important;
|
||||||
border-color: #dc3545 !important;
|
border-color: #dc3545 !important;
|
||||||
color: #721c24;
|
color: #721c24;
|
||||||
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.3);
|
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ref {
|
.ref {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #555;
|
color: #555;
|
||||||
padding-top: 0.25rem;
|
padding-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.verse-button.correct ~ .ref {
|
.verse-button.correct ~ .ref {
|
||||||
color: #28a745;
|
color: #28a745;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.verse-button.wrong ~ .ref {
|
.verse-button.wrong ~ .ref {
|
||||||
color: #dc3545;
|
color: #dc3545;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result {
|
.result {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result button,
|
.result button,
|
||||||
.error button {
|
.error button {
|
||||||
padding: 0.75rem 2rem;
|
padding: 0.75rem 2rem;
|
||||||
background: #007bff;
|
background: #007bff;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result button:hover,
|
.result button:hover,
|
||||||
.error button:hover {
|
.error button:hover {
|
||||||
background: #0056b3;
|
background: #0056b3;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<link rel="icon" href={favicon} />
|
<link rel="icon" href={favicon} />
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="Bibdle RSS Feed" href="/feed.xml" />
|
||||||
<script
|
<script
|
||||||
defer
|
defer
|
||||||
src="https://umami.snail.city/script.js"
|
src="https://umami.snail.city/script.js"
|
||||||
|
|||||||
@@ -437,7 +437,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<!-- <title>Bibdle — A daily bible game{isDev ? " (dev)" : ""}</title> -->
|
|
||||||
<title>A daily bible game{isDev ? " (dev)" : ""}</title>
|
<title>A daily bible game{isDev ? " (dev)" : ""}</title>
|
||||||
<!-- <meta
|
<!-- <meta
|
||||||
name="description"
|
name="description"
|
||||||
|
|||||||
145
src/routes/feed.xml/+server.ts
Normal file
145
src/routes/feed.xml/+server.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { dailyVerses } from '$lib/server/db/schema';
|
||||||
|
import { desc } from 'drizzle-orm';
|
||||||
|
|
||||||
|
// Helper: Escape XML special characters
|
||||||
|
function escapeXml(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Format YYYY-MM-DD to RFC 822 date string
|
||||||
|
function formatRFC822(dateStr: string): string {
|
||||||
|
// Parse date in America/New_York timezone (EST/EDT)
|
||||||
|
// Assuming midnight ET
|
||||||
|
const date = new Date(dateStr + 'T00:00:00-05:00');
|
||||||
|
return date.toUTCString().replace('GMT', 'EST');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Format YYYY-MM-DD to readable date
|
||||||
|
function formatReadableDate(dateStr: string): string {
|
||||||
|
const date = new Date(dateStr + 'T00:00:00');
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
timeZone: 'America/New_York'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Format verse text (VerseDisplay + Imposter unbalanced punctuation handling)
|
||||||
|
function formatVerseText(text: string): string {
|
||||||
|
let formatted = text;
|
||||||
|
|
||||||
|
// Handle unbalanced opening/closing punctuation (from Imposter.svelte)
|
||||||
|
const pairs: [string, string][] = [
|
||||||
|
['(', ')'],
|
||||||
|
['[', ']'],
|
||||||
|
['{', '}'],
|
||||||
|
['"', '"'],
|
||||||
|
["'", "'"],
|
||||||
|
['\u201C', '\u201D'], // " "
|
||||||
|
['\u2018', '\u2019'] // ' '
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if text starts with opening punctuation without closing
|
||||||
|
for (const [open, close] of pairs) {
|
||||||
|
if (formatted.startsWith(open) && !formatted.includes(close)) {
|
||||||
|
formatted += '...' + close;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if text ends with closing punctuation without opening
|
||||||
|
for (const [open, close] of pairs) {
|
||||||
|
if (formatted.endsWith(close) && !formatted.includes(open)) {
|
||||||
|
formatted = open + '...' + formatted;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if text contains unbalanced opening quotes (not at start) without closing
|
||||||
|
for (const [open, close] of pairs) {
|
||||||
|
const openCount = (formatted.match(new RegExp(`\\${open}`, 'g')) || []).length;
|
||||||
|
const closeCount = (formatted.match(new RegExp(`\\${close}`, 'g')) || []).length;
|
||||||
|
if (openCount > closeCount) {
|
||||||
|
formatted += close;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capitalize first letter if lowercase (from VerseDisplay.svelte)
|
||||||
|
formatted = formatted.replace(/^([a-z])/, (c) => c.toUpperCase());
|
||||||
|
|
||||||
|
// Replace trailing punctuation with ellipsis
|
||||||
|
// Preserve closing quotes/brackets that may have been added
|
||||||
|
formatted = formatted.replace(/[,:;-—]([)\]}\"\'\u201D\u2019]*)$/, '...$1');
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ request }) => {
|
||||||
|
try {
|
||||||
|
// Query last 30 verses, ordered by date descending
|
||||||
|
const verses = await db
|
||||||
|
.select()
|
||||||
|
.from(dailyVerses)
|
||||||
|
.orderBy(desc(dailyVerses.date))
|
||||||
|
.limit(30);
|
||||||
|
|
||||||
|
// Generate ETag based on latest verse date
|
||||||
|
const etag = verses[0]?.date ? `"bibdle-feed-${verses[0].date}"` : '"bibdle-feed-empty"';
|
||||||
|
|
||||||
|
// Check if client has cached version
|
||||||
|
if (request.headers.get('If-None-Match') === etag) {
|
||||||
|
return new Response(null, { status: 304 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get site URL from environment or use default
|
||||||
|
const SITE_URL = process.env.PUBLIC_SITE_URL || 'https://bibdle.com';
|
||||||
|
|
||||||
|
// Build RSS XML
|
||||||
|
const lastBuildDate = verses[0] ? formatRFC822(verses[0].date) : new Date().toUTCString();
|
||||||
|
|
||||||
|
const items = verses
|
||||||
|
.map(
|
||||||
|
(verse) => `
|
||||||
|
<item>
|
||||||
|
<title>Bibdle verse for ${formatReadableDate(verse.date)}</title>
|
||||||
|
<description>${escapeXml(formatVerseText(verse.verseText))}</description>
|
||||||
|
<link>${SITE_URL}</link>
|
||||||
|
<guid isPermaLink="false">bibdle-verse-${verse.date}</guid>
|
||||||
|
<pubDate>${formatRFC822(verse.date)}</pubDate>
|
||||||
|
</item>`
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const xml = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>Bibdle</title>
|
||||||
|
<link>${SITE_URL}</link>
|
||||||
|
<description>A daily Bible game</description>
|
||||||
|
<language>en-us</language>
|
||||||
|
<lastBuildDate>${lastBuildDate}</lastBuildDate>
|
||||||
|
<ttl>720</ttl>${items}
|
||||||
|
</channel>
|
||||||
|
</rss>`;
|
||||||
|
|
||||||
|
return new Response(xml, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/rss+xml; charset=utf-8',
|
||||||
|
'Cache-Control': 'public, max-age=3600, s-maxage=3600',
|
||||||
|
ETag: etag
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('RSS feed generation error:', error);
|
||||||
|
return new Response('Internal Server Error', { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user