feat: added rss fc8c359e
Steve · 2026-01-26 21:22 4 file(s) · +90 −0
bun.lock +7 −0
6 6
      "name": "steve-photo-svelte",
7 7
      "dependencies": {
8 8
        "exifr": "^7.1.3",
9 +
        "feed": "^5.2.0",
9 10
        "from": "^0.1.7",
10 11
        "import": "^0.0.6",
11 12
      },
300 301
301 302
    "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
302 303
304 +
    "feed": ["feed@5.2.0", "", { "dependencies": { "xml-js": "^1.6.11" } }, "sha512-hgH6CCb+7+0c8PBlakI2KubG6R+Rb1MhpNcdvqUXZTBwBHf32piwY255diAkAmkGZ6AWlywOU88AkOgP9q8Rdw=="],
305 +
303 306
    "from": ["from@0.1.7", "", {}, "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g=="],
304 307
305 308
    "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
372 375
373 376
    "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
374 377
378 +
    "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="],
379 +
375 380
    "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
376 381
377 382
    "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
417 422
    "wrangler": ["wrangler@4.60.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.11.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.0", "miniflare": "4.20260120.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260120.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260120.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-n4kibm/xY0Qd5G2K/CbAQeVeOIlwPNVglmFjlDRCCYk3hZh8IggO/rg8AXt/vByK2Sxsugl5Z7yvgWxrUbmS6g=="],
418 423
419 424
    "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
425 +
426 +
    "xml-js": ["xml-js@1.6.11", "", { "dependencies": { "sax": "^1.2.4" }, "bin": { "xml-js": "./bin/cli.js" } }, "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g=="],
420 427
421 428
    "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
422 429
package.json +1 −0
26 26
	},
27 27
	"dependencies": {
28 28
		"exifr": "^7.1.3",
29 +
		"feed": "^5.2.0",
29 30
		"from": "^0.1.7",
30 31
		"import": "^0.0.6"
31 32
	}
src/lib/feed.ts (added) +32 −0
1 +
import type { ImageItem } from "$lib/types";
2 +
3 +
const R2_BASE_URL = "https://r2.steve.photo";
4 +
5 +
export async function getPhotos(platform: App.Platform | undefined): Promise<ImageItem[]> {
6 +
  const db = platform?.env?.DB;
7 +
8 +
  if (!db) {
9 +
    return [];
10 +
  }
11 +
12 +
  const result = await db.prepare("SELECT * FROM photos ORDER BY date DESC").all();
13 +
14 +
  const photos: ImageItem[] = result.results.map((row: Record<string, unknown>) => ({
15 +
    slug: row.slug as string,
16 +
    title: row.title as string,
17 +
    date: row.date as string,
18 +
    image: `${R2_BASE_URL}/${row.image_key}`,
19 +
    thumb: `${R2_BASE_URL}/${row.thumb_key}`,
20 +
    type: row.type as string,
21 +
    camera: row.camera as string,
22 +
    lens: row.lens as string,
23 +
    aperture: row.aperture as string,
24 +
    exposure: row.exposure as string,
25 +
    focalLength: row.focal_length as string,
26 +
    iso: row.iso as string,
27 +
    make: row.make as string,
28 +
    tags: [],
29 +
  }));
30 +
31 +
  return photos;
32 +
}
src/routes/rss.xml/+server.ts (added) +50 −0
1 +
import { Feed } from "feed";
2 +
import { getPhotos } from "$lib/feed";
3 +
import type { RequestHandler } from "./$types";
4 +
5 +
export const GET: RequestHandler = async ({ platform }) => {
6 +
  const feed = new Feed({
7 +
    title: "steve.photo",
8 +
    description: "Personal photography portolio of Steve Simkins",
9 +
    id: "https://steve.photo",
10 +
    link: "https://steve.photo/",
11 +
    language: "en",
12 +
    favicon: "https://steve.photo/favicon.ico",
13 +
    copyright: `Copyright ${new Date().getFullYear().toString()}, Steve Simkins`,
14 +
    feedLinks: {
15 +
      rss: "https://steve.photo/rss.xml",
16 +
    },
17 +
    author: {
18 +
      name: "Steve Simkins",
19 +
      email: "contact@stevedylan.dev",
20 +
      link: "https://stevedylan.dev",
21 +
    },
22 +
    ttl: 60,
23 +
  });
24 +
25 +
  const photos = await getPhotos(platform);
26 +
27 +
  for (const photo of photos) {
28 +
    feed.addItem({
29 +
      title: photo.title,
30 +
      id: `https://steve.photo/photo/${photo.slug}`,
31 +
      link: `https://steve.photo/photo/${photo.slug}`,
32 +
      date: new Date(photo.date),
33 +
      image: photo.image,
34 +
      author: [
35 +
        {
36 +
          name: "Steve Simkins",
37 +
          email: "contact@stevedylan.dev",
38 +
          link: "https://stevedylan.dev",
39 +
        },
40 +
      ],
41 +
      content: `<img src="${photo.image}" alt="${photo.title}" /><p>Camera: ${photo.camera} | Lens: ${photo.lens} | ${photo.aperture} | ${photo.exposure} | ISO ${photo.iso}</p>`,
42 +
    });
43 +
  }
44 +
45 +
  return new Response(feed.rss2(), {
46 +
    headers: {
47 +
      "Content-Type": "application/xml; charset=utf-8",
48 +
    },
49 +
  });
50 +
};