diff --git a/AGENTS.md b/AGENTS.md
index ea2e2c7d..16d702d8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -67,6 +67,21 @@ Preferred verification flow for docs/content changes:
## Docs Authoring Rules
+### User Journey Funnel
+
+Maintain a directed "funnel" for documentation to maximize user success and conversion:
+
+1. **Phase 1: Quickstart (Local Demo)** — The primary entry point. Run `html2rss-web` with Docker and generate a feed from a page URL in minutes.
+2. **Phase 2: Production (Deployment)** — The goal for invested users. Move to a stable, production-ready instance.
+3. **Phase 3: Refinement (Custom Configs)** — Secondary optimization. Author custom YAML configs only when automatic generation needs precise control.
+
+**Rules for Funnel Maintenance:**
+
+- Avoid branching paths in introductory pages; always point toward the next phase in the funnel.
+- Define "html2rss-web" as the primary interface and "page-to-RSS" as the primary workflow.
+- Use "Feed Directory" consistently to refer to the pre-built feed catalog; avoid terms like "catalog", "included feeds", or "packaged configs" in user-facing docs.
+- Do not introduce new terminology (e.g., "toolkit") or unrelated infrastructure concepts (e.g., "custom domains") unless they are essential to a specific guide.
+
### Code Snippets
In docs content (`src/content/docs/**`) and docs-supporting components:
diff --git a/astro.config.mjs b/astro.config.mjs
index 2cb7056f..b1775a3c 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -13,6 +13,19 @@ export default defineConfig({
"/components/html2rss": "/ruby-gem/",
"/components/html2rss-configs": "/creating-custom-feeds/",
"/components": "/",
+ "/web-application/how-to/deployment": "/web-application/deployment/",
+ "/web-application/how-to/automatic-updates": "/web-application/deployment/#auto-update-with-watchtower",
+ "/web-application/how-to/use-automatic-feed-generation":
+ "/web-application/guides/use-the-feed-directory/",
+ "/web-application/how-to/use-automatic-feed-generation/":
+ "/web-application/guides/use-the-feed-directory/",
+ "/web-application/how-to": "/web-application/guides/",
+ "/ruby-gem/how-to/dynamic-parameters": "/ruby-gem/guides/dynamic-parameters/",
+ "/ruby-gem/how-to/dynamic-parameters/": "/ruby-gem/guides/dynamic-parameters/",
+ "/ruby-gem/how-to": "/ruby-gem/guides/",
+ "/ruby-gem/tutorials": "/ruby-gem/guides/",
+ "/ruby-gem/tutorials/": "/ruby-gem/guides/",
+ "/web-application/guides/use-included-configs": "/web-application/guides/use-the-feed-directory/",
},
build: {
inlineStylesheets: "auto",
@@ -249,7 +262,7 @@ export default defineConfig({
sidebar: [
{
label: "Getting Started",
- link: "/getting-started",
+ link: "/getting-started/",
},
{
label: "Feed Directory",
@@ -257,7 +270,7 @@ export default defineConfig({
},
{
label: "Create Custom Feeds",
- link: "/creating-custom-feeds",
+ link: "/creating-custom-feeds/",
},
{
label: "Web Application",
@@ -265,18 +278,15 @@ export default defineConfig({
items: [
"web-application",
"web-application/getting-started",
+ "web-application/deployment",
{
- label: "How-to",
- autogenerate: { directory: "web-application/how-to" },
+ label: "Guides",
+ autogenerate: { directory: "web-application/guides" },
},
{
label: "Reference",
autogenerate: { directory: "web-application/reference" },
},
- {
- label: "Tutorials",
- autogenerate: { directory: "web-application/tutorials" },
- },
],
},
{
@@ -286,22 +296,22 @@ export default defineConfig({
"ruby-gem",
"ruby-gem/installation",
{
- label: "How-to",
- autogenerate: { directory: "ruby-gem/how-to" },
- },
- {
- label: "Reference",
- autogenerate: { directory: "ruby-gem/reference" },
+ label: "Guides",
+ autogenerate: { directory: "ruby-gem/guides" },
},
{
label: "Tutorials",
autogenerate: { directory: "ruby-gem/tutorials" },
},
+ {
+ label: "Reference",
+ autogenerate: { directory: "ruby-gem/reference" },
+ },
],
},
{
label: "About",
- link: "/about",
+ link: "/about/",
},
{
label: "Get Involved",
diff --git a/examples/deployment/.env b/examples/deployment/.env
index 62f8201d..2d62b809 100644
--- a/examples/deployment/.env
+++ b/examples/deployment/.env
@@ -1,26 +1,14 @@
-# Domain & routing
-CADDY_HOST=example.com
-
-# Core runtime
-RACK_ENV=production
-
-# Security
+# Production secrets
# Generate with: openssl rand -hex 32
HTML2RSS_SECRET_KEY=replace-with-64-hex-characters-generated-by-openssl-rand-hex-32
-# Authenticated health endpoint token
-# Required by the documented Compose stack.
-# If you build a custom stack and probe only /api/v1/health/live and /api/v1/health/ready,
-# you can omit this value.
-HEALTH_CHECK_TOKEN=replace-with-strong-health-token
-
-# Auto source (optional; keep false unless you need automatic feed generation)
-AUTO_SOURCE_ENABLED=false
+# Web UI / feed creation token
+# Paste this into the web app when it asks for an access token.
+HTML2RSS_ACCESS_TOKEN=replace-with-strong-access-token
-# Observability (optional)
-#SENTRY_DSN=
+# Optional authenticated health token
+# Set this only if you plan to use GET /api/v1/health instead of /api/v1/health/ready.
+# HEALTH_CHECK_TOKEN=replace-with-strong-health-token
-# Performance tuning (override defaults only when needed)
-WEB_CONCURRENCY=2
-WEB_MAX_THREADS=5
-RACK_TIMEOUT_SERVICE_TIMEOUT=15
+# `AUTO_SOURCE_ENABLED=true` and `BOTASAURUS_SCRAPER_URL=http://botasaurus:4010`
+# come from docker-compose.yml in this example.
diff --git a/examples/deployment/docker-compose.yml b/examples/deployment/docker-compose.yml
index 3774a619..3b796c3b 100644
--- a/examples/deployment/docker-compose.yml
+++ b/examples/deployment/docker-compose.yml
@@ -6,43 +6,13 @@ services:
- path: .env
required: false
environment:
+ RACK_ENV: production
PORT: 4000
+ HTML2RSS_SECRET_KEY: ${HTML2RSS_SECRET_KEY:?set HTML2RSS_SECRET_KEY}
+ HTML2RSS_ACCESS_TOKEN: ${HTML2RSS_ACCESS_TOKEN:?set HTML2RSS_ACCESS_TOKEN}
+ AUTO_SOURCE_ENABLED: "true"
BOTASAURUS_SCRAPER_URL: http://botasaurus:4010
botasaurus:
image: html2rss/botasaurus-scrape-api:latest
restart: unless-stopped
-
- caddy:
- image: caddy:2-alpine
- depends_on:
- - html2rss-web
- command:
- - caddy
- - reverse-proxy
- - --from
- - ${CADDY_HOST}
- - --to
- - html2rss-web:4000
- ports:
- - "80:80"
- - "443:443"
- volumes:
- - caddy_data:/data
-
- watchtower:
- image: containrrr/watchtower
- depends_on:
- - html2rss-web
- - caddy
- - botasaurus
- command:
- - --cleanup
- - --interval
- - "7200"
- volumes:
- - /var/run/docker.sock:/var/run/docker.sock:ro
- restart: unless-stopped
-
-volumes:
- caddy_data:
diff --git a/src/components/docs/AutoGenerationOptional.astro b/src/components/docs/AutoGenerationOptional.astro
index 982682bb..225e558b 100644
--- a/src/components/docs/AutoGenerationOptional.astro
+++ b/src/components/docs/AutoGenerationOptional.astro
@@ -3,6 +3,7 @@ import { Aside } from "@astrojs/starlight/components";
---
diff --git a/src/components/docs/DockerComposeSnippet.astro b/src/components/docs/DockerComposeSnippet.astro
index 0ced6c48..ee161f5c 100644
--- a/src/components/docs/DockerComposeSnippet.astro
+++ b/src/components/docs/DockerComposeSnippet.astro
@@ -1,6 +1,6 @@
---
import { Code } from "@astrojs/starlight/components";
-import { botasaurusImage, browserlessImage, caddyImage, watchtowerImage, webImage } from "../../data/docker";
+import { botasaurusImage, caddyImage, watchtowerImage, webImage } from "../../data/docker";
interface Props {
variant: "minimal" | "productionCaddy" | "secure" | "watchtower" | "resourceGuardrails";
@@ -12,35 +12,15 @@ const snippets: Record = {
minimal: `services:
html2rss-web:
image: ${webImage}
- restart: unless-stopped
ports:
- "127.0.0.1:4000:4000"
- env_file:
- - path: .env
- required: false
environment:
- RACK_ENV: production
- PORT: 4000
- HTML2RSS_SECRET_KEY: \${HTML2RSS_SECRET_KEY:?set HTML2RSS_SECRET_KEY}
- HEALTH_CHECK_TOKEN: \${HEALTH_CHECK_TOKEN:?set HEALTH_CHECK_TOKEN}
- SENTRY_DSN: \${SENTRY_DSN:-}
- BROWSERLESS_IO_WEBSOCKET_URL: ws://browserless:4002
- BROWSERLESS_IO_API_TOKEN: \${BROWSERLESS_IO_API_TOKEN:?set BROWSERLESS_IO_API_TOKEN}
+ RACK_ENV: development
+ HTML2RSS_ACCESS_TOKEN: CHANGE_ME_ADMIN_TOKEN
BOTASAURUS_SCRAPER_URL: http://botasaurus:4010
botasaurus:
- image: ${botasaurusImage}
- restart: unless-stopped
-
- browserless:
- image: "${browserlessImage}"
- restart: unless-stopped
- ports:
- - "127.0.0.1:4002:4002"
- environment:
- PORT: 4002
- CONCURRENT: 10
- TOKEN: \${BROWSERLESS_IO_API_TOKEN:?set BROWSERLESS_IO_API_TOKEN}`,
+ image: ${botasaurusImage}`,
productionCaddy: `services:
caddy:
image: ${caddyImage}
@@ -68,24 +48,15 @@ const snippets: Record = {
RACK_ENV: production
PORT: 4000
HTML2RSS_SECRET_KEY: \${HTML2RSS_SECRET_KEY:?set HTML2RSS_SECRET_KEY}
- HEALTH_CHECK_TOKEN: \${HEALTH_CHECK_TOKEN:?set HEALTH_CHECK_TOKEN}
+ HTML2RSS_ACCESS_TOKEN: \${HTML2RSS_ACCESS_TOKEN:?set HTML2RSS_ACCESS_TOKEN}
+ AUTO_SOURCE_ENABLED: "true"
SENTRY_DSN: \${SENTRY_DSN:-}
- BROWSERLESS_IO_WEBSOCKET_URL: ws://browserless:4002
- BROWSERLESS_IO_API_TOKEN: \${BROWSERLESS_IO_API_TOKEN:?set BROWSERLESS_IO_API_TOKEN}
BOTASAURUS_SCRAPER_URL: http://botasaurus:4010
botasaurus:
image: ${botasaurusImage}
restart: unless-stopped
- browserless:
- image: "${browserlessImage}"
- restart: unless-stopped
- environment:
- PORT: 4002
- CONCURRENT: 10
- TOKEN: \${BROWSERLESS_IO_API_TOKEN:?set BROWSERLESS_IO_API_TOKEN}
-
volumes:
caddy_data:`,
secure: `services:
@@ -99,23 +70,14 @@ volumes:
RACK_ENV: production
PORT: 4000
HTML2RSS_SECRET_KEY: \${HTML2RSS_SECRET_KEY:?set HTML2RSS_SECRET_KEY}
- HEALTH_CHECK_TOKEN: \${HEALTH_CHECK_TOKEN:?set HEALTH_CHECK_TOKEN}
+ HTML2RSS_ACCESS_TOKEN: \${HTML2RSS_ACCESS_TOKEN:?set HTML2RSS_ACCESS_TOKEN}
+ AUTO_SOURCE_ENABLED: "true"
SENTRY_DSN: \${SENTRY_DSN:-}
- BROWSERLESS_IO_WEBSOCKET_URL: ws://browserless:4002
- BROWSERLESS_IO_API_TOKEN: \${BROWSERLESS_IO_API_TOKEN:?set BROWSERLESS_IO_API_TOKEN}
BOTASAURUS_SCRAPER_URL: http://botasaurus:4010
botasaurus:
image: ${botasaurusImage}
- restart: unless-stopped
-
- browserless:
- image: "${browserlessImage}"
- restart: unless-stopped
- environment:
- PORT: 4002
- CONCURRENT: 10
- TOKEN: \${BROWSERLESS_IO_API_TOKEN:?set BROWSERLESS_IO_API_TOKEN}`,
+ restart: unless-stopped`,
watchtower: `services:
watchtower:
image: ${watchtowerImage}
@@ -124,7 +86,7 @@ volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
# Optional for private registries only:
# - "\${HOME}/.docker/config.json:/config.json:ro"
- command: --cleanup --interval 7200 html2rss-web botasaurus browserless caddy`,
+ command: --cleanup --interval 7200 html2rss-web botasaurus caddy`,
resourceGuardrails: `services:
html2rss-web:
image: ${webImage}
diff --git a/src/content/docs/common-use-cases.mdx b/src/content/docs/common-use-cases.mdx
index 4f58cc49..931b1a8a 100644
--- a/src/content/docs/common-use-cases.mdx
+++ b/src/content/docs/common-use-cases.mdx
@@ -91,6 +91,6 @@ Follow multiple open source projects and their updates.
## Next Steps
-- **[Run html2rss-web with Docker](/web-application/getting-started)** to verify your own instance.
-- **[Use automatic feed generation](/web-application/how-to/use-automatic-feed-generation/)** when you want direct page-URL conversion.
+- **[Run html2rss-web with Docker](/web-application/getting-started/)** to verify your own instance.
+- **[Use automatic feed generation](/web-application/guides/use-automatic-feed-generation/)** when you want direct page-URL conversion.
- **[Create custom feeds](/creating-custom-feeds/)** when you need stable, reviewable extraction rules.
diff --git a/src/content/docs/creating-custom-feeds.mdx b/src/content/docs/creating-custom-feeds.mdx
index 0d782f8d..16382e65 100644
--- a/src/content/docs/creating-custom-feeds.mdx
+++ b/src/content/docs/creating-custom-feeds.mdx
@@ -9,7 +9,7 @@ import { Aside, Code } from "@astrojs/starlight/components";
When existing feeds or auto-sourcing are not enough, write a YAML config for the site you want to follow.
-**Prerequisites:** You should be familiar with the [Getting Started](/getting-started) guide before diving into custom configurations.
+**Prerequisites:** You should be familiar with the [Getting Started](/getting-started/) guide before diving into custom configurations.