Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
d516efe
fix: vercel
Niapya Jul 5, 2026
f643be2
fix: vercel
Niapya Jul 5, 2026
609e926
fix: vercel
Niapya Jul 5, 2026
929878c
fix: test vercel
Niapya Jul 5, 2026
baa9448
fix: vercel
Niapya Jul 5, 2026
31d001e
fix: use bun on vercel
Niapya Jul 5, 2026
b26c77b
fix: add output
Niapya Jul 5, 2026
fac17f1
fix: vercel
Niapya Jul 5, 2026
85b1bff
fix: cloudflare
Niapya Jul 5, 2026
28b76d0
fix: update rssbook dependency to workspace references and clean up c…
Niapya Jul 5, 2026
96b20ae
fix: netlify
Niapya Jul 5, 2026
cc87328
feat: improve style
Niapya Jul 5, 2026
489572b
feat: add i18n support and reorganize theme components
Niapya Jul 5, 2026
07e37e9
feat: add multi-theme system with gallery, magazine, masonry, minimal…
Niapya Jul 6, 2026
451ba52
theme
Niapya Jul 6, 2026
7c35013
feat: env improvement
Niapya Jul 6, 2026
2f02777
doc: improve readme document.
Niapya Jul 6, 2026
fc6e18d
docs: update README to clarify environment variable sources and AI as…
Niapya Jul 6, 2026
c137bf4
feat: sanitize HTML descriptions in Footer and Nav components across …
Niapya Jul 6, 2026
5af6cdf
fix: xss issue
Niapya Jul 6, 2026
1faa523
feat: update deployment configurations and clean up unused parameters
Niapya Jul 6, 2026
6160fe0
fix lint
Niapya Jul 6, 2026
a194573
doc: improve document
Niapya Jul 6, 2026
a9ebab0
doc: fix cloudflare
Niapya Jul 6, 2026
38650c9
doc: fix cloudflare
Niapya Jul 6, 2026
459a874
fix: fix platform deploy, remove netlify
Niapya Jul 6, 2026
ba61ea6
fix: fix openapi load slowly
Niapya Jul 6, 2026
553303f
feat: improve openapi document
Niapya Jul 7, 2026
4a52ea2
init
Niapya Jul 7, 2026
eda5ddc
init
Niapya Jul 7, 2026
14507b8
add hosts
Niapya Jul 7, 2026
e2d2cf6
init
Niapya Jul 8, 2026
1380351
init
Niapya Jul 8, 2026
017a3e7
init
Niapya Jul 8, 2026
4778181
init
Niapya Jul 9, 2026
9e44d15
broswer
Niapya Jul 11, 2026
06e0211
cache
Niapya Jul 11, 2026
396d29b
style: normalize project formatting
Niapya Jul 13, 2026
80cbdc9
format
Niapya Jul 24, 2026
442e176
fix: use extensionless Deno cache test import
Niapya Jul 24, 2026
7f7bd20
Merge remote-tracking branch 'origin/main' into dev
Niapya Jul 24, 2026
6e620a2
chore: update version to 1.2.0 in package.json
Niapya Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .agents/skills/create-feed/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ Use this skill when adding or updating a Feed route in RSSBook, migrating route
2. Create the Source at `pkgs/rssbook/src/routers/feeds/{category}/{slug}/index.ts`; use `bun source:new` when scaffolding from an existing template is useful.
3. Register it in `pkgs/rssbook/src/routers/feeds/{category}/index.ts`.
4. Use RSSBook utilities from the handler context instead of importing alternate fetch/parser stacks.
5. Add one explicit route test per Feed route.
6. Run formatting and TypeScript. Run source route tests with `bun source:test` or `bun source:test:all` only when route network checks are intended.
5. In `route_handler`, prefer predefined errors from `@/utils/error` over `new Error(...)` so the global handler can map `status`, `code`, and `retryable` consistently.
6. When a route fails because of invalid input, missing upstream content, browser availability, cache misses, or parse/render issues, choose the closest existing `RSSBookError` subclass first; add a new one in `@/utils/error` only when no existing code fits.
7. Add one explicit route test per Feed route.
8. Run formatting and TypeScript. Run source route tests with `bun source:test` or `bun source:test:all` only when route network checks are intended.

## Feed Development Checklist

Expand All @@ -34,6 +36,8 @@ Read only the references needed for the task:

- New Source structure, route migration patterns, metadata naming, and route testing: [references/feed-routes.md](references/feed-routes.md)
- RSSBook utility APIs (`cache`, `date`, `ofetch`, `load`, `formatHTML`, feed transforms): [references/utils.md](references/utils.md)
- Browser routes, bounded page concurrency, lifecycle, cookies, response waits, resource filtering, and fingerprint consistency: [references/browser.md](references/browser.md)
- RSSBook error catalog and where to use each error: [references/errors.md](references/errors.md)

## Source Shape

Expand Down Expand Up @@ -118,6 +122,7 @@ The handler function receives these properties:
- **Metadata**: `meta` (source metadata including `slug`, `title`, `description`, `domain`, `config`), `lang` (request language)
- **Request**: `params` (route parameters), `query` (query parameters), `headers` (request headers)
- **Utilities**: `cache`, `date`, `ofetch`, `load`, `formatHTML`, `toAbsoluteURL`, `parse`, `logger`, `uuid`, `config`
- **Browser**: `browser` for routes whose Feed metadata declares `browser: true`; read [references/browser.md](references/browser.md) before using it

## Metadata Rules

Expand Down
183 changes: 183 additions & 0 deletions .agents/skills/create-feed/references/browser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Browser Routes

Use the browser only when the upstream page requires JavaScript, browser cookies, or an API response that cannot be reproduced reliably with `ofetch`. Prefer `ofetch` and `load` for static pages.

RSSBook exposes an app-scoped `Browser` pool backed by Puppeteer-compatible APIs. A route receives `browser` after its Feed metadata declares `browser: true`.

```typescript
export default new Source({
description: "Rendered feed example.",
domain: "example.com",
slug: "rendered-example",
title: "Rendered Example",
}).feed(
{
browser: true,
description: "Fetch content rendered by the browser.",
title: "Rendered Example",
},
(app) => app.get("/latest", async ({ browser }) => {
// Browser route implementation
}),
);
```

## Single Page Lifecycle

Use `browser.acquirePage()` for a single isolated page. It consumes one context and one page slot. Always close the lease in `finally`; closing the page lease also closes its private context.

```typescript
const lease = await browser.acquirePage();

try {
const { page } = lease;
await page.goto(link, { waitUntil: "domcontentloaded" });
return await page.content();
} finally {
await lease.close();
}
```

RSSBook calls `browser.deinit()` when the application stops. That shutdown hook is a final cleanup boundary, not a replacement for closing route-owned leases.

## Concurrency Model

The Browser instance has three fixed capacity properties:

- `maxBrowsers`: maximum physical local browser processes or remote browser sessions.
- `maxContextsPerBrowser`: maximum isolated contexts in each physical browser.
- `maxPagesPerContext`: maximum tracked pages, including popups, in each context.

When capacity is full, `acquireContext()` and `acquirePage()` wait until another lease releases capacity. Pass an `AbortSignal` when the wait must follow a request timeout or cancellation.

```typescript
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(new Error("Browser acquisition timed out")), 15_000);

try {
const lease = await browser.acquirePage({ signal: controller.signal });
try {
await lease.page.goto(link, { waitUntil: "domcontentloaded" });
} finally {
await lease.close();
}
} finally {
clearTimeout(timeout);
}
```

`browser.acquirePage()` creates a private context for every page. Therefore its maximum concurrent page count is constrained by context capacity. Use it for isolated tasks and unrelated requests.

Use `browser.acquireContext()` when pages need to share cookies, local storage, or login state. Pages within one context may run concurrently, but the route must bound workers to `browser.maxPagesPerContext`.

```typescript
const contextLease = await browser.acquireContext();

try {
const results = new Array<string>(links.length);
const concurrency = Math.min(links.length, browser.maxPagesPerContext);

await Promise.all(
Array.from({ length: concurrency }, async (_, workerIndex) => {
for (let index = workerIndex; index < links.length; index += concurrency) {
const pageLease = await contextLease.acquirePage();
try {
await pageLease.page.goto(links[index], { waitUntil: "domcontentloaded" });
results[index] = await pageLease.page.content();
} finally {
await pageLease.close();
}
}
}),
);

return results;
} finally {
await contextLease.close();
}
```

Do not acquire an unbounded list of page leases with `Promise.all` and hold every acquired page until all acquisitions finish. If the list exceeds `maxPagesPerContext`, the remaining acquisitions wait while completed acquisitions retain the capacity they need, causing a deadlock. Use bounded workers and close each page before acquiring the next one.

## Browser Utilities

Browser utilities are exported from `@/utils`. They operate on Puppeteer `Page` or `BrowserContext` objects and remain separate from the Browser pool.

### Block Resources

Images, media, and fonts are blocked by default when no resource types are supplied.

```typescript
import { blockResources } from "@/utils";

await blockResources(page);
await page.goto(link, { waitUntil: "domcontentloaded" });
```

Use `allowResources(page, { resourceTypes: [...] })` when only a small set should load. Configure interception before navigation.

### Wait For A Response

`waitForResponse()` registers the response listener before navigation or interaction, avoiding a race with fast responses.

```typescript
import { waitForResponse } from "@/utils";

const response = await waitForResponse(
page,
(response) => response.url().includes("/api/items") && response.status() === 200,
() => page.goto(link, { waitUntil: "domcontentloaded" }),
{ timeout: 15_000 },
);
const data = await response.json();
```

### Preserve Cookies

Cookies belong to a BrowserContext. Reuse the same context for pages that must share a session. Routes control persistence themselves; RSSBook does not provide a global browser state store.

```typescript
import { getCookieHeader, setCookieHeader } from "@/utils";

await setCookieHeader(contextLease.context, configuredCookie, "https://example.com");
const cookieHeader = await getCookieHeader(contextLease.context);
```

Store a returned Cookie header with the route's chosen cache or configuration mechanism only when that behavior is required. Do not share unrelated users' authenticated state.

### Keep Fingerprints Consistent

Use native Puppeteer methods together with RSSBook helpers before the first navigation. Treat the values as one coherent device profile; do not randomize fields independently or change only the User-Agent.

```typescript
import {
setColorDepth,
setDeviceMemory,
setHardwareConcurrency,
setLanguages,
} from "@/utils";

await page.setUserAgent({
platform: "Win32",
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
});
await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 1 });
await page.emulateTimezone("America/New_York");
await setLanguages(page, ["en-US", "en"]);
await setHardwareConcurrency(page, 8);
await setDeviceMemory(page, 8);
await setColorDepth(page, 24);
```

The User-Agent, platform, languages, timezone, screen size, hardware concurrency, device memory, and color depth must describe a plausible single machine. Puppeteer and the browser already provide `navigator.vendor`, plugins, permissions, and Chrome APIs; overriding them independently is likely to make the fingerprint less consistent.

## Route Checklist

- Declare `browser: true` in Feed metadata.
- Acquire through `browser.acquirePage()` or `browser.acquireContext()`; do not launch Puppeteer inside a route.
- Configure cookies, interception, response waits, and fingerprint values before navigation.
- Bound multi-page work to `browser.maxPagesPerContext`.
- Close every page or context lease in `finally`.
- Prefer one context when pages must share a session; prefer private page leases for isolated tasks.
- Wrap expensive browser work in `cache.tryGet()` when the result can be reused safely.
53 changes: 53 additions & 0 deletions .agents/skills/create-feed/references/errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# RSSBook Error Reference

Use `@/utils/error` for route handlers, utilities, and the global Elysia error handler. Prefer the narrowest error that matches the failure mode.

## Browser

- `BrowserUnavailableError`: route was declared with `browser: true`, but the app was created without browser support.
- `BrowserClosedError`: a browser instance was already deinitialized and cannot be reused.
- `BrowserInitializationError`: browser startup returned a non-browser or otherwise failed before the browser became usable.
- `BrowserEndpointError`: CDP/WebSocket endpoint resolution failed or returned an invalid browser description.
- `UnsupportedProtocolError`: browser endpoint URL uses an unsupported protocol.

## Cache And Fetch

- `CacheMissError`: a cache lookup was required, but no fetcher was provided.
- `FetchHtmlError`: HTML fetch or HTML parsing for a page scrape failed.
- `SourceFetchError`: a feed source could not fetch or parse any upstream feed data.

## Input And Validation

- `InvalidUrlError`: a URL string cannot be parsed.
- `InvalidProtocolError`: a URL is not `http:` or `https:`.
- `LocalAddressError` and `LocalIpAddressError`: local addresses and IP literals are rejected.
- `InvalidDomainSuffixError` and `InvalidDomainNameError`: the host is not an allowed public domain.
- `InvalidRoutePathError`: a dispatch path does not start with `/`.
- `InvalidSourceSlugError`: a source slug does not match the allowed slug format.
- `DuplicateRouteTitleError` and `DuplicateSourceError`: route or source registration collides with an existing one.

## Feed Parsing And Rendering

- `ParseError`: raw JSON parsing or feed parsing failed.
- `DataValidationError`: parsed feed data did not satisfy the `Data` schema.
- `InvalidFeedFormatError` and `FeedFormatError`: the requested feed format is invalid or unsupported.
- `FeedRenderError`: feed serialization failed after validation.

## Route Utilities

- `InvalidOverrideJsonError`: a utility route override payload is invalid JSON.
- `UnionFeedError`, `IntersectionFeedError`, `FilterFeedError`, `SortFeedError`, `TransformFeedError`: combine, intersect, filter, sort, or transform route logic failed after upstream work started.
- `InvalidRankingTypeError`: the requested source-specific ranking type is not supported.
- `FeedNotFoundError`: an upstream source page, author, tag, or user could not be found.

## Status Guidance

- Use `400` for client input and validation failures.
- Use `404` for missing upstream resources.
- Use `502` for upstream fetch or endpoint resolution failures.
- Use `503` when the failure is caused by unavailable runtime capabilities.
- Use `500` for internal or invariant violations.

## Skill Rule

When adding a new route handler, start by checking this reference and use an existing error class if one matches. Add a new error only when the failure mode has a stable meaning that will be reused elsewhere.
4 changes: 3 additions & 1 deletion HOSTS
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
url,description,online
https://rssbook.htu.me,Official RSSBook Website,true
https://rssbook.vercel.app,Official RSSBook Instance on Vercel,true
https://rssbook.niapya.deno.net,Official RSSBook Instance on Deno,true
https://rssbook.htu.me,Official RSSBook Instance on Cloudflare,true
2 changes: 1 addition & 1 deletion README.EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,6 @@ This project would not be possible without the support and help of all contribut
[![Github Contributors](https://contrib.rocks/image?repo=HackHTU/RSSBook)](https://github.com/HackHTU/RSSBook/graphs/contributors)

This project's inspiration comes from RSSHub and RSSWorker.
This project's development relies on Bun, Cheerio, dayjs, ElysiaJS, Kita/html, ofetch, sanitize-html, unstorage and other excellent open-source projects.
This project's development relies on Bun, Cheerio, dayjs, ElysiaJS, Kita/html, lru-cache, ofetch, sanitize-html, and other excellent open-source projects.

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,6 @@ RSSBook 本身只负责「生成 Feed」,但由于输出的就是标准 RSS 2.
[![Github Contributors](https://contrib.rocks/image?repo=HackHTU/RSSBook)](https://github.com/HackHTU/RSSBook/graphs/contributors)

本项目的灵感离不开 RSSHub 和 RSSWorker 项目。
本项目的开发离不开 Bun, Cheerio, dayjs, ElysiaJS, Kita/html, ofetch, sanitize-html, unstorage 优秀的开源项目
本项目的开发离不开 Bun, Cheerio, dayjs, ElysiaJS, Kita/html, lru-cache, ofetch, sanitize-html 等优秀的开源项目

本项目采用 MIT 许可证,详情请查看 [LICENSE](LICENSE) 文件。
11 changes: 3 additions & 8 deletions biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,13 @@
"./pkgs/*/package.json",
"./pkgs/*/tsconfig.json",
"./pkgs/*/src/**",
"./platform/*/index.ts",
"./platform/*/package.json",
"./platform/*/tsconfig.json",
"./platform/cloudflare/wrangler.jsonc",
"./platform/deno/deno.json",
"./platform/netlify/netlify.toml",
"./platform/vercel/vercel.json",
"./platform/**",
"./scripts/**",
"./biome.jsonc",
"./package.json",
"./tsconfig.json",
"./tsconfig.base.json"
"./tsconfig.base.json",
"!**/worker-configuration.d.ts"
]
},
"formatter": {
Expand Down
Loading
Loading