-
Subscribe to Scouting411 Pulse
+
Subscribe with your Feed Reader
Although the upstream sources on Pulse are published in a variety of
From 4378c408915c7fb8224054d7428461ec23201241 Mon Sep 17 00:00:00 2001
From: kevin8181 <66894759+kevin8181@users.noreply.github.com>
Date: Thu, 12 Mar 2026 23:23:24 -0400
Subject: [PATCH 2/8] change query api to POST, add bruno
---
bruno/bruno/bruno.json | 6 ++++++
bruno/bruno/collection.bru | 4 ++++
bruno/bruno/query-posts.bru | 17 +++++++++++++++++
src/features/postsQuery/query.ts | 6 ++----
src/pages/api/posts.ts | 25 +++++--------------------
5 files changed, 34 insertions(+), 24 deletions(-)
create mode 100644 bruno/bruno/bruno.json
create mode 100644 bruno/bruno/collection.bru
create mode 100644 bruno/bruno/query-posts.bru
diff --git a/bruno/bruno/bruno.json b/bruno/bruno/bruno.json
new file mode 100644
index 0000000..565cc3e
--- /dev/null
+++ b/bruno/bruno/bruno.json
@@ -0,0 +1,6 @@
+{
+ "version": "1",
+ "name": "bruno",
+ "type": "collection",
+ "ignore": ["node_modules", ".git"]
+}
diff --git a/bruno/bruno/collection.bru b/bruno/bruno/collection.bru
new file mode 100644
index 0000000..e3e7518
--- /dev/null
+++ b/bruno/bruno/collection.bru
@@ -0,0 +1,4 @@
+meta {
+ name: bruno
+ type: collection
+}
diff --git a/bruno/bruno/query-posts.bru b/bruno/bruno/query-posts.bru
new file mode 100644
index 0000000..0a191f8
--- /dev/null
+++ b/bruno/bruno/query-posts.bru
@@ -0,0 +1,17 @@
+meta {
+ name: Query Posts
+ type: http
+ seq: 1
+}
+
+post {
+ url: http://localhost:4321/api/posts
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+
+ }
+}
diff --git a/src/features/postsQuery/query.ts b/src/features/postsQuery/query.ts
index 6465d00..9e22918 100644
--- a/src/features/postsQuery/query.ts
+++ b/src/features/postsQuery/query.ts
@@ -19,8 +19,6 @@ export async function queryPosts(
}
type QueryOpts = z.infer;
-//todo
-//eslint-disable-next-line
-const queryOptsSchema = z.object({
- paginate: paginateOptsSchema,
+export const queryOptsSchema = z.object({
+ paginate: paginateOptsSchema.default({ page: 1, pageSize: 20 }),
});
diff --git a/src/pages/api/posts.ts b/src/pages/api/posts.ts
index a0a50a5..6a706fc 100644
--- a/src/pages/api/posts.ts
+++ b/src/pages/api/posts.ts
@@ -1,16 +1,11 @@
export const prerender = false;
import type { APIRoute } from "astro";
-import { z } from "astro/zod";
-import { queryPosts } from "@/features/postsQuery/query";
+import { queryPosts, queryOptsSchema } from "@/features/postsQuery/query";
-export const GET: APIRoute = async (context) => {
- const params = context.url.searchParams;
+export const POST: APIRoute = async (context) => {
+ const body = await context.request.json();
- console.log(params);
-
- const paramsObj = Object.fromEntries(params);
-
- const { error, data: query } = postsQueryParamsSchema.safeParse(paramsObj);
+ const { error, data: query } = queryOptsSchema.safeParse(body);
if (error) {
return new Response(JSON.stringify({ error }), {
@@ -21,12 +16,7 @@ export const GET: APIRoute = async (context) => {
});
}
- const posts = await queryPosts({
- paginate: {
- page: query.page,
- pageSize: query.pageSize,
- },
- });
+ const posts = await queryPosts(query);
return new Response(JSON.stringify(posts), {
status: 200,
@@ -35,8 +25,3 @@ export const GET: APIRoute = async (context) => {
},
});
};
-
-const postsQueryParamsSchema = z.object({
- page: z.coerce.number().min(1),
- pageSize: z.coerce.number().min(1).max(1000).default(20),
-});
From bc36f1921925f426523658e37970ca3f36b6c7c4 Mon Sep 17 00:00:00 2001
From: kevin8181 <66894759+kevin8181@users.noreply.github.com>
Date: Thu, 12 Mar 2026 23:33:18 -0400
Subject: [PATCH 3/8] fix bruno folders
---
bruno/{bruno => }/bruno.json | 0
bruno/{bruno => }/collection.bru | 0
bruno/{bruno => }/query-posts.bru | 0
3 files changed, 0 insertions(+), 0 deletions(-)
rename bruno/{bruno => }/bruno.json (100%)
rename bruno/{bruno => }/collection.bru (100%)
rename bruno/{bruno => }/query-posts.bru (100%)
diff --git a/bruno/bruno/bruno.json b/bruno/bruno.json
similarity index 100%
rename from bruno/bruno/bruno.json
rename to bruno/bruno.json
diff --git a/bruno/bruno/collection.bru b/bruno/collection.bru
similarity index 100%
rename from bruno/bruno/collection.bru
rename to bruno/collection.bru
diff --git a/bruno/bruno/query-posts.bru b/bruno/query-posts.bru
similarity index 100%
rename from bruno/bruno/query-posts.bru
rename to bruno/query-posts.bru
From fd3a533ba3a3a220aea78b59a6bc049f9df2299f Mon Sep 17 00:00:00 2001
From: kevin8181 <66894759+kevin8181@users.noreply.github.com>
Date: Thu, 12 Mar 2026 23:54:30 -0400
Subject: [PATCH 4/8] add sorting to query api
---
src/features/postsQuery/query.ts | 14 +++++++-----
src/features/postsQuery/sort.ts | 25 ++++++++++++++++++++++
src/util/{paginate.ts => paginateArray.ts} | 6 +++---
3 files changed, 37 insertions(+), 8 deletions(-)
create mode 100644 src/features/postsQuery/sort.ts
rename src/util/{paginate.ts => paginateArray.ts} (91%)
diff --git a/src/features/postsQuery/query.ts b/src/features/postsQuery/query.ts
index 9e22918..7d05283 100644
--- a/src/features/postsQuery/query.ts
+++ b/src/features/postsQuery/query.ts
@@ -1,24 +1,28 @@
import { FeedManager } from "@/features/feeds/feedManager";
import { z } from "astro/zod";
import {
- paginate,
+ paginateArray,
paginateOptsSchema,
type PaginatedResults,
-} from "@/util/paginate";
+} from "@/util/paginateArray";
+import { sortPosts, sortOptsSchema } from "@/features/postsQuery/sort";
import { Post } from "@/features/posts/post";
export async function queryPosts(
opts: QueryOpts,
): Promise> {
- // get all the posts. todo make it so you can start with only a subset of the feeds
+ // todo make it so you can start with only a subset of the feeds
const posts = await FeedManager.allPosts();
- // todo add filtering and sorting
+ // todo add filtering
- return paginate(posts, opts.paginate);
+ const sortedPosts = sortPosts(posts, opts.sort);
+
+ return paginateArray(sortedPosts, opts.paginate);
}
type QueryOpts = z.infer;
export const queryOptsSchema = z.object({
+ sort: sortOptsSchema.default({ mode: "date", direction: "desc" }),
paginate: paginateOptsSchema.default({ page: 1, pageSize: 20 }),
});
diff --git a/src/features/postsQuery/sort.ts b/src/features/postsQuery/sort.ts
new file mode 100644
index 0000000..1f65794
--- /dev/null
+++ b/src/features/postsQuery/sort.ts
@@ -0,0 +1,25 @@
+import { z } from "astro/zod";
+import { Post } from "@/features/posts/post";
+
+export function sortPosts(posts: Post[], opts: z.infer) {
+ let sortedPosts;
+
+ switch (opts.mode) {
+ case "date":
+ sortedPosts = posts.sort((a, b) => a.date.getTime() - b.date.getTime());
+ break;
+ default:
+ return posts;
+ }
+
+ if (opts.direction === "desc") {
+ sortedPosts.reverse();
+ }
+
+ return sortedPosts;
+}
+
+export const sortOptsSchema = z.object({
+ mode: z.enum(["date"]),
+ direction: z.enum(["asc", "desc"]),
+});
diff --git a/src/util/paginate.ts b/src/util/paginateArray.ts
similarity index 91%
rename from src/util/paginate.ts
rename to src/util/paginateArray.ts
index e0f06f3..1be235b 100644
--- a/src/util/paginate.ts
+++ b/src/util/paginateArray.ts
@@ -1,7 +1,7 @@
import { z } from "astro/zod";
/** paginate an array of items */
-export function paginate(
+export function paginateArray(
data: T[],
opts: PaginateOpts,
): PaginatedResults {
@@ -27,9 +27,9 @@ export function paginate(
type PaginateOpts = z.infer;
export const paginateOptsSchema = z.object({
/** the page size */
- pageSize: z.number().min(1),
+ pageSize: z.coerce.number().min(1),
/** the page number */
- page: z.number().min(1),
+ page: z.coerce.number().min(1),
});
export type PaginatedResults = {
From d0dded0246fcb9f1274ef51e72f6c4138f031040 Mon Sep 17 00:00:00 2001
From: kevin8181 <66894759+kevin8181@users.noreply.github.com>
Date: Fri, 13 Mar 2026 00:37:01 -0400
Subject: [PATCH 5/8] add filtering to post query api
---
bruno/query-posts.bru | 12 ++++-
src/features/postsQuery/filter.ts | 87 ++++++++++++++++++-------------
src/features/postsQuery/query.ts | 8 +--
src/util/utilTypes.ts | 3 ++
4 files changed, 71 insertions(+), 39 deletions(-)
diff --git a/bruno/query-posts.bru b/bruno/query-posts.bru
index 0a191f8..d0361ff 100644
--- a/bruno/query-posts.bru
+++ b/bruno/query-posts.bru
@@ -12,6 +12,16 @@ post {
body:json {
{
-
+ "filter": {
+ "keyword": "scouting"
+ },
+ "sort": {
+ "mode": "date",
+ "direction": "desc"
+ },
+ "paginate": {
+ "page": 1,
+ "pageSize": 20
+ }
}
}
diff --git a/src/features/postsQuery/filter.ts b/src/features/postsQuery/filter.ts
index 235061e..c16d019 100644
--- a/src/features/postsQuery/filter.ts
+++ b/src/features/postsQuery/filter.ts
@@ -1,35 +1,52 @@
-// import { z } from "astro/zod";
-// import { Post } from "@/features/posts/post";
-
-// export function filterPosts({
-// posts,
-// filterOpts,
-// }: {
-// posts: Post[];
-// filterOpts: z.infer;
-// }) {
-// return posts.filter((_post) => {
-// const predicates = buildPredicates(filterOpts);
-
-// // if all of the predicates are true, return true
-// if (Object.values(predicates).every((predicate) => predicate)) {
-// return true;
-// }
-
-// // if any of the predicates are false, return false
-// return false;
-// });
-// }
-
-// /** check a post against each filter. return an object with a bool for each predicate */
-// function buildPredicates(filters: FilterOpts) {
-// return {
-
-// };
-// }
-
-// export const filterOptsSchema = z.object({
-// dateBefore: z.coerce.date().optional(),
-// dateAfter: z.coerce.date().optional(),
-// }).strict();
-// export type FilterOpts = z.infer;
+import { z } from "astro/zod";
+import { Post } from "@/features/posts/post";
+import type { Predicate } from "@/util/utilTypes";
+
+export function filterPosts(
+ posts: Post[],
+ opts: z.infer,
+) {
+ const predicates = buildPredicates(opts);
+
+ return posts.filter((post) =>
+ predicates.every((predicate) => predicate(post)),
+ );
+}
+
+/** turn a filter config into an array of predicates */
+function buildPredicates(filter: FilterOpts): Predicate[] {
+ const predicates: Predicate[] = [];
+
+ for (const [key, value] of Object.entries(filter) as [
+ keyof FilterOpts,
+ FilterOpts[keyof FilterOpts],
+ ][]) {
+ if (value !== undefined) {
+ const factory = predicateFactories[key];
+ predicates.push(factory(value as NonNullable));
+ }
+ }
+
+ return predicates;
+}
+
+const predicateFactories: PredicateFactories = {
+ keyword: (value: string): Predicate => {
+ return (post) => {
+ const titleMatch = post.title.toUpperCase().includes(value.toUpperCase());
+ const descriptionMatch = !!post.description
+ ?.toUpperCase()
+ .includes(value.toUpperCase());
+
+ return titleMatch || descriptionMatch;
+ };
+ },
+};
+type PredicateFactories = {
+ [K in keyof F]-?: (value: NonNullable) => Predicate;
+};
+
+type FilterOpts = z.infer;
+export const filterOptsSchema = z.object({
+ keyword: z.string().optional(),
+});
diff --git a/src/features/postsQuery/query.ts b/src/features/postsQuery/query.ts
index 7d05283..e9f239a 100644
--- a/src/features/postsQuery/query.ts
+++ b/src/features/postsQuery/query.ts
@@ -1,11 +1,12 @@
import { FeedManager } from "@/features/feeds/feedManager";
import { z } from "astro/zod";
+import { sortPosts, sortOptsSchema } from "@/features/postsQuery/sort";
import {
paginateArray,
paginateOptsSchema,
type PaginatedResults,
} from "@/util/paginateArray";
-import { sortPosts, sortOptsSchema } from "@/features/postsQuery/sort";
+import { filterPosts, filterOptsSchema } from "@/features/postsQuery/filter";
import { Post } from "@/features/posts/post";
export async function queryPosts(
@@ -14,15 +15,16 @@ export async function queryPosts(
// todo make it so you can start with only a subset of the feeds
const posts = await FeedManager.allPosts();
- // todo add filtering
+ const filteredPosts = filterPosts(posts, opts.filter);
- const sortedPosts = sortPosts(posts, opts.sort);
+ const sortedPosts = sortPosts(filteredPosts, opts.sort);
return paginateArray(sortedPosts, opts.paginate);
}
type QueryOpts = z.infer;
export const queryOptsSchema = z.object({
+ filter: filterOptsSchema.default({}),
sort: sortOptsSchema.default({ mode: "date", direction: "desc" }),
paginate: paginateOptsSchema.default({ page: 1, pageSize: 20 }),
});
diff --git a/src/util/utilTypes.ts b/src/util/utilTypes.ts
index f15465f..fdbe7f2 100644
--- a/src/util/utilTypes.ts
+++ b/src/util/utilTypes.ts
@@ -1 +1,4 @@
export type UrlShaped = `${"http" | "https"}://${string}`;
+
+/** a predicate function that takes an item and returns a boolean */
+export type Predicate = (item: T) => boolean;
From 78d83ae78a933b1ad747224fa00d5e7d44031a36 Mon Sep 17 00:00:00 2001
From: kevin8181 <66894759+kevin8181@users.noreply.github.com>
Date: Fri, 13 Mar 2026 02:21:16 -0400
Subject: [PATCH 6/8] improve opml
---
public/xslt/opml.xslt | 15 +++++++++++++++
src/pages/feeds/all/opml.ts | 3 ++-
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/public/xslt/opml.xslt b/public/xslt/opml.xslt
index 59bd3a7..23eeb63 100644
--- a/public/xslt/opml.xslt
+++ b/public/xslt/opml.xslt
@@ -9,11 +9,26 @@
+
+
+
+
diff --git a/src/pages/feeds/all/opml.ts b/src/pages/feeds/all/opml.ts
index 189dada..bc60984 100644
--- a/src/pages/feeds/all/opml.ts
+++ b/src/pages/feeds/all/opml.ts
@@ -8,12 +8,13 @@ export const GET: APIRoute = async (context) => {
head: {
title: "Scouting411 News Sources",
ownerName: "Scouting411",
- dateModified: new Date(),
+ dateCreated: new Date(),
},
body: {
outlines: FeedManager.feeds.map((feed) => ({
text: feed.name,
title: feed.name,
+ description: feed.description,
htmlUrl: new URL(feed.overviewUrl, context.site).toString(),
xmlUrl: new URL(feed.rssUrl, context.site).toString(),
type: "rss",
From 1063e2678aa6372d218e8a396cfd725cd98fe399 Mon Sep 17 00:00:00 2001
From: kevin8181 <66894759+kevin8181@users.noreply.github.com>
Date: Fri, 13 Mar 2026 02:21:32 -0400
Subject: [PATCH 7/8] add persistent nanostore for post query options
---
package.json | 2 ++
pnpm-lock.yaml | 22 ++++++++++++++++++++++
src/features/postsQuery/filter.ts | 1 +
src/features/postsQuery/query.ts | 2 +-
src/stores/postsQuery.ts | 19 +++++++++++++++++++
5 files changed, 45 insertions(+), 1 deletion(-)
create mode 100644 src/stores/postsQuery.ts
diff --git a/package.json b/package.json
index 3895ac8..b6cc25b 100644
--- a/package.json
+++ b/package.json
@@ -30,11 +30,13 @@
"@fortawesome/free-brands-svg-icons": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0",
"@fortawesome/react-fontawesome": "^0.2.0",
+ "@nanostores/persistent": "^1.3.3",
"@tailwindcss/vite": "^4.2.1",
"@upstash/redis": "^1.36.4",
"astro": "^6.0.3",
"feedsmith": "2.9.0",
"he": "^1.2.0",
+ "nanostores": "^1.1.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"tailwindcss": "^4.2.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 05ed576..754a0d7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -32,6 +32,9 @@ importers:
'@fortawesome/react-fontawesome':
specifier: ^0.2.0
version: 0.2.6(@fortawesome/fontawesome-svg-core@7.2.0)(react@19.2.4)
+ '@nanostores/persistent':
+ specifier: ^1.3.3
+ version: 1.3.3(nanostores@1.1.1)
'@tailwindcss/vite':
specifier: ^4.2.1
version: 4.2.1(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))
@@ -47,6 +50,9 @@ importers:
he:
specifier: ^1.2.0
version: 1.2.0
+ nanostores:
+ specifier: ^1.1.1
+ version: 1.1.1
react:
specifier: ^19.2.4
version: 19.2.4
@@ -688,6 +694,12 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ '@nanostores/persistent@1.3.3':
+ resolution: {integrity: sha512-+b4I8xrmjhKE3hQ9V7/b4Xa+MBMkM2P4Ulv33zFEF/+2Hucsb24vTjYiWR8R97y8YdRptmRKlL5Qwy0q1Jj5nQ==}
+ engines: {node: ^20.0.0 || >=22.0.0}
+ peerDependencies:
+ nanostores: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^1.0.0
+
'@napi-rs/wasm-runtime@1.1.1':
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
@@ -2218,6 +2230,10 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ nanostores@1.1.1:
+ resolution: {integrity: sha512-EYJqS25r2iBeTtGQCHidXl1VfZ1jXM7Q04zXJOrMlxVVmD0ptxJaNux92n1mJ7c5lN3zTq12MhH/8x59nP+qmg==}
+ engines: {node: ^20.0.0 || >=22.0.0}
+
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -3683,6 +3699,10 @@ snapshots:
- encoding
- supports-color
+ '@nanostores/persistent@1.3.3(nanostores@1.1.1)':
+ dependencies:
+ nanostores: 1.1.1
+
'@napi-rs/wasm-runtime@1.1.1':
dependencies:
'@emnapi/core': 1.8.1
@@ -5454,6 +5474,8 @@ snapshots:
nanoid@3.3.11: {}
+ nanostores@1.1.1: {}
+
natural-compare@1.4.0: {}
neotraverse@0.6.18: {}
diff --git a/src/features/postsQuery/filter.ts b/src/features/postsQuery/filter.ts
index c16d019..90ad122 100644
--- a/src/features/postsQuery/filter.ts
+++ b/src/features/postsQuery/filter.ts
@@ -41,6 +41,7 @@ const predicateFactories: PredicateFactories = {
return titleMatch || descriptionMatch;
};
},
+ //todo add filters for datebefore, dateafter
};
type PredicateFactories = {
[K in keyof F]-?: (value: NonNullable) => Predicate;
diff --git a/src/features/postsQuery/query.ts b/src/features/postsQuery/query.ts
index e9f239a..925896a 100644
--- a/src/features/postsQuery/query.ts
+++ b/src/features/postsQuery/query.ts
@@ -22,7 +22,7 @@ export async function queryPosts(
return paginateArray(sortedPosts, opts.paginate);
}
-type QueryOpts = z.infer;
+export type QueryOpts = z.infer;
export const queryOptsSchema = z.object({
filter: filterOptsSchema.default({}),
sort: sortOptsSchema.default({ mode: "date", direction: "desc" }),
diff --git a/src/stores/postsQuery.ts b/src/stores/postsQuery.ts
new file mode 100644
index 0000000..a819ab4
--- /dev/null
+++ b/src/stores/postsQuery.ts
@@ -0,0 +1,19 @@
+import { persistentJSON } from "@nanostores/persistent";
+import type { QueryOpts } from "@/features/postsQuery/query";
+
+const defaultQueryOpts: QueryOpts = {
+ filter: {},
+ sort: {
+ mode: "date",
+ direction: "desc",
+ },
+ paginate: {
+ page: 1,
+ pageSize: 20,
+ },
+};
+
+export const $postsQuery = persistentJSON(
+ "postsQuery",
+ defaultQueryOpts,
+);
From 43bbc3846ea1e46e02f8e10a7f4c4e49705175ef Mon Sep 17 00:00:00 2001
From: kevin8181 <66894759+kevin8181@users.noreply.github.com>
Date: Fri, 13 Mar 2026 13:08:04 -0400
Subject: [PATCH 8/8] add atom feeds, better dx for urls attached to feeds,
switch browser over to query api
---
src/components/layout/Head.astro | 2 +-
src/components/layout/sidebar/sidebar.tsx | 12 ++-
src/components/react/post.tsx | 2 +-
src/features/feeds/feed.ts | 31 +++++---
src/features/postsQuery/query.ts | 2 +-
src/pages/feeds/[slug]/atom.ts | 80 +++++++++++++++++++
src/pages/feeds/[slug]/rss.ts | 4 +-
src/pages/feeds/all/opml.ts | 5 +-
src/pages/pulse/browse/[page]/_page.tsx | 13 ---
src/pages/pulse/browse/[page]/index.astro | 31 --------
src/pages/pulse/browse/_page.tsx | 31 ++++++++
src/pages/pulse/browse/index.astro | 8 ++
src/pages/pulse/sources/[slug]/index.astro | 7 +-
src/pages/pulse/sources/index.astro | 4 +-
src/pages/pulse/stats/index.astro | 93 ++++++++++++----------
src/pages/pulse/subscribe/index.astro | 58 +++++++++-----
src/stores/postsQuery.ts | 2 +-
src/util/paginateArray.ts | 19 +++--
18 files changed, 265 insertions(+), 139 deletions(-)
create mode 100644 src/pages/feeds/[slug]/atom.ts
delete mode 100644 src/pages/pulse/browse/[page]/_page.tsx
delete mode 100644 src/pages/pulse/browse/[page]/index.astro
create mode 100644 src/pages/pulse/browse/_page.tsx
create mode 100644 src/pages/pulse/browse/index.astro
diff --git a/src/components/layout/Head.astro b/src/components/layout/Head.astro
index d3adc0f..9a2f71c 100644
--- a/src/components/layout/Head.astro
+++ b/src/components/layout/Head.astro
@@ -34,7 +34,7 @@ import { FeedManager } from "@/features/feeds/feedManager";
rel="alternate"
type="application/rss+xml"
title={feed.name}
- href={feed.rssUrl}
+ href={feed.urls.rss}
/>
))
}
diff --git a/src/components/layout/sidebar/sidebar.tsx b/src/components/layout/sidebar/sidebar.tsx
index 2d778a4..2b20f02 100644
--- a/src/components/layout/sidebar/sidebar.tsx
+++ b/src/components/layout/sidebar/sidebar.tsx
@@ -4,6 +4,7 @@ import { NavDivider } from "@/components/layout/sidebar/navDivider";
import {
faBookBookmark,
faBullhorn,
+ faCommentDots,
faHome,
faMagnifyingGlassChart,
faNewspaper,
@@ -24,7 +25,7 @@ export default function Sidebar({ url }: { url: URL }) {
+
-
+
{post.feed.name}
diff --git a/src/features/feeds/feed.ts b/src/features/feeds/feed.ts
index b3b42a0..03345f1 100644
--- a/src/features/feeds/feed.ts
+++ b/src/features/feeds/feed.ts
@@ -18,7 +18,7 @@ export class Feed {
readonly name: string;
readonly slug: string;
readonly description: string;
- readonly homepageUrl: UrlShaped;
+ private readonly _homepageUrl: UrlShaped;
private readonly _adapter: FeedAdapter;
// LIFECYCLE
@@ -27,7 +27,7 @@ export class Feed {
this.name = opts.name;
this.slug = opts.slug;
this.description = opts.description;
- this.homepageUrl = opts.homepageUrl;
+ this._homepageUrl = opts.homepageUrl;
this._adapter = opts.adapter;
}
@@ -36,13 +36,26 @@ export class Feed {
get type() {
return this._adapter.type;
}
- /** relative href to the detail page for this feed */
- get overviewUrl() {
- return `/pulse/sources/${this.slug}`;
- }
- /** relative href to the generated rss feed */
- get rssUrl() {
- return `/feeds/${this.slug}/rss`;
+ // /** relative href to the detail page for this feed */
+ // get overviewUrl() {
+ // return `/pulse/sources/${this.slug}`;
+ // }
+ // /** relative href to the generated rss feed */
+ // get rssUrl() {
+ // return `/feeds/${this.slug}/rss`;
+ // }
+
+ get urls() {
+ return {
+ /** relative href to the detail page for this feed */
+ overview: `/pulse/sources/${this.slug}`,
+ /** relative href to the generated rss feed */
+ rss: `/feeds/${this.slug}/rss`,
+ /** relative href to the generated atom feed */
+ atom: `/feeds/${this.slug}/atom`,
+ /** upstream's html homepage */
+ homepage: this._homepageUrl,
+ };
}
// INSTANCE METHODS
diff --git a/src/features/postsQuery/query.ts b/src/features/postsQuery/query.ts
index 925896a..b8431ec 100644
--- a/src/features/postsQuery/query.ts
+++ b/src/features/postsQuery/query.ts
@@ -26,5 +26,5 @@ export type QueryOpts = z.infer;
export const queryOptsSchema = z.object({
filter: filterOptsSchema.default({}),
sort: sortOptsSchema.default({ mode: "date", direction: "desc" }),
- paginate: paginateOptsSchema.default({ page: 1, pageSize: 20 }),
+ paginate: paginateOptsSchema.default({ page: 1, maxPageSize: 20 }),
});
diff --git a/src/pages/feeds/[slug]/atom.ts b/src/pages/feeds/[slug]/atom.ts
new file mode 100644
index 0000000..9b536a6
--- /dev/null
+++ b/src/pages/feeds/[slug]/atom.ts
@@ -0,0 +1,80 @@
+import type { APIRoute } from "astro";
+import { FeedManager } from "@/features/feeds/feedManager";
+import { generateAtomFeed } from "feedsmith";
+
+export function getStaticPaths() {
+ return FeedManager.feeds.map((feed) => ({
+ params: { slug: feed.slug },
+ }));
+}
+
+export const GET: APIRoute = async (context) => {
+ const slug = context.params.slug!;
+ const feed = FeedManager.getFeedBySlug(slug)!;
+ const posts = await feed.posts();
+
+ const generated = generateAtomFeed(
+ {
+ title: feed.name,
+ id: feed.urls.homepage,
+ updated: new Date(),
+
+ links: [
+ {
+ rel: "self",
+ href: feed.urls.overview,
+ type: "text/html",
+ title: feed.name,
+ hreflang: "en-us",
+ },
+ {
+ rel: "alternate",
+ href: feed.urls.rss,
+ type: "application/rss+xml",
+ title: "RSS Feed",
+ hreflang: "en-us",
+ },
+ {
+ rel: "alternate",
+ href: feed.urls.homepage,
+ type: "text/html",
+ title: "Upstream Homepage",
+ hreflang: "en-us",
+ },
+ ],
+ entries: posts.map((post) => ({
+ title: post.title,
+ id: post.url,
+ updated: post.date,
+ ...(post.description && { description: post.description }),
+ published: post.date,
+
+ links: [
+ {
+ rel: "self",
+ href: post.url,
+ type: "text/html",
+ title: post.title,
+ hreflang: "en-us",
+ },
+ ],
+ })),
+ },
+ {
+ stylesheets: [
+ // todo this styleshee doesn't seem to play nice with atom
+ // {
+ // title: "RSS Stylesheet",
+ // type: "text/xsl",
+ // href: "/xslt/rss.xslt",
+ // },
+ ],
+ },
+ );
+
+ return new Response(generated, {
+ headers: {
+ "Content-Type": "text/xml",
+ },
+ });
+};
diff --git a/src/pages/feeds/[slug]/rss.ts b/src/pages/feeds/[slug]/rss.ts
index ccba167..a76f78a 100644
--- a/src/pages/feeds/[slug]/rss.ts
+++ b/src/pages/feeds/[slug]/rss.ts
@@ -16,7 +16,7 @@ export const GET: APIRoute = async (context) => {
const generated = generateRssFeed(
{
title: feed.name,
- description: feed.homepageUrl, //todo add a description to each feed
+ description: feed.description,
items: posts.map((post) => ({
title: post.title,
@@ -30,7 +30,7 @@ export const GET: APIRoute = async (context) => {
],
source: {
title: feed.name,
- url: feed.homepageUrl, //todo I think this is supposed to be an rss feed
+ url: feed.urls.homepage, //todo I think this is supposed to be an rss feed
},
})),
},
diff --git a/src/pages/feeds/all/opml.ts b/src/pages/feeds/all/opml.ts
index bc60984..f000add 100644
--- a/src/pages/feeds/all/opml.ts
+++ b/src/pages/feeds/all/opml.ts
@@ -15,8 +15,9 @@ export const GET: APIRoute = async (context) => {
text: feed.name,
title: feed.name,
description: feed.description,
- htmlUrl: new URL(feed.overviewUrl, context.site).toString(),
- xmlUrl: new URL(feed.rssUrl, context.site).toString(),
+ htmlUrl: new URL(feed.urls.overview, context.site).toString(),
+ //todo make an opml for atom feeds?
+ xmlUrl: new URL(feed.urls.rss, context.site).toString(),
type: "rss",
language: "en-us",
})),
diff --git a/src/pages/pulse/browse/[page]/_page.tsx b/src/pages/pulse/browse/[page]/_page.tsx
deleted file mode 100644
index 64b4439..0000000
--- a/src/pages/pulse/browse/[page]/_page.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { CardList } from "@/components/react/cardList";
-import { RenderPost as PostComponent } from "@/components/react/post";
-import type { Post } from "@/features/posts/post";
-
-export async function BrowsePage({ posts }: { posts: Post[] }) {
- return (
-
- {posts.map((post) => (
-
- ))}
-
- );
-}
diff --git a/src/pages/pulse/browse/[page]/index.astro b/src/pages/pulse/browse/[page]/index.astro
deleted file mode 100644
index 28793ea..0000000
--- a/src/pages/pulse/browse/[page]/index.astro
+++ /dev/null
@@ -1,31 +0,0 @@
----
-import UiLayout from "@/components/layout/UiLayout.astro";
-import { BrowsePage } from "@/pages/pulse/browse/[page]/_page";
-import type { GetStaticPaths } from "astro";
-import { FeedManager } from "@/features/feeds/feedManager";
-
-export const getStaticPaths = (async ({ paginate }) => {
- const posts = await FeedManager.allPosts();
-
- return paginate(posts, {
- pageSize: 20,
- });
-}) satisfies GetStaticPaths;
-
-const { page } = Astro.props;
-
-const lastItem = page.size * page.currentPage;
-const firstItem = lastItem - page.size + 1;
----
-
-
- Showing posts {firstItem} - {lastItem} of {page.total}.
-
-
-
diff --git a/src/pages/pulse/browse/_page.tsx b/src/pages/pulse/browse/_page.tsx
new file mode 100644
index 0000000..ed50cef
--- /dev/null
+++ b/src/pages/pulse/browse/_page.tsx
@@ -0,0 +1,31 @@
+import { CardList } from "@/components/react/cardList";
+import { RenderPost as PostComponent } from "@/components/react/post";
+import { queryPosts } from "@/features/postsQuery/query";
+
+export async function BrowsePage() {
+ const results = await queryPosts({
+ filter: {},
+ sort: {
+ mode: "date",
+ direction: "desc",
+ },
+ paginate: {
+ maxPageSize: 100,
+ page: 1,
+ },
+ });
+ return (
+ <>
+
+ Showing posts {results.pagination.firstItemIndex + 1} -{" "}
+ {results.pagination.lastItemIndex + 1} of{" "}
+ {results.pagination.totalItems}.
+
+
+ {results.items.map((post) => (
+
+ ))}
+
+ >
+ );
+}
diff --git a/src/pages/pulse/browse/index.astro b/src/pages/pulse/browse/index.astro
new file mode 100644
index 0000000..c539a8d
--- /dev/null
+++ b/src/pages/pulse/browse/index.astro
@@ -0,0 +1,8 @@
+---
+import UiLayout from "@/components/layout/UiLayout.astro";
+import { BrowsePage } from "@/pages/pulse/browse/_page";
+---
+
+
+
+
diff --git a/src/pages/pulse/sources/[slug]/index.astro b/src/pages/pulse/sources/[slug]/index.astro
index 4b12d6c..6461857 100644
--- a/src/pages/pulse/sources/[slug]/index.astro
+++ b/src/pages/pulse/sources/[slug]/index.astro
@@ -21,15 +21,16 @@ if (!feed) throw new Error(`Feed ${slug} not found`);
{feed.description}
Homepage: {feed.homepageUrl} {feed.urls.homepage}
Indexed posts: {(await feed.posts()).length}
- Subcribe: rss
+ Subcribe: rss
+ atom