Skip to content

feat: add sitemap.xml and robots.txt for SEO - #55

Merged
jb55 merged 18 commits into
damus-io:masterfrom
alltheseas:feat/sitemap-seo
Feb 16, 2026
Merged

feat: add sitemap.xml and robots.txt for SEO#55
jb55 merged 18 commits into
damus-io:masterfrom
alltheseas:feat/sitemap-seo

Conversation

@alltheseas

@alltheseas alltheseas commented Dec 25, 2025

Copy link
Copy Markdown
Contributor

Summary

Adds dynamic sitemap generation and robots.txt to improve search engine discoverability of Nostr content.

Closes #26

New Routes

Route Content-Type Cache Description
/robots.txt text/plain 24h Crawler directives + sitemap reference
/sitemap.xml application/xml 1h Dynamic sitemap from nostrdb cache

Sitemap Content

Queries local nostrdb cache for:

  • Notes (kind:1) → note1xxx URLs, 90-day lookback
  • Articles (kind:30023) → naddr1xxx URLs, 365-day lookback (evergreen content)
  • Profiles (kind:0) → npub1xxx URLs

Limited to 10,000 URLs per content type.

robots.txt

User-agent: *
Allow: /
Allow: /.well-known/nostr.json
Disallow: /metrics
Disallow: /*.json

Sitemap: https://damus.io/sitemap.xml

Prometheus Metrics

Metric Type Description
sitemap_generations_total counter Generation requests
sitemap_generation_duration_seconds gauge Time to generate
sitemap_urls_total gauge Total URLs
sitemap_notes_count gauge Notes included
sitemap_articles_count gauge Articles included
sitemap_profiles_count gauge Profiles included

Configuration

NOTECRUMBS_BASE_URL=https://damus.io  # Required for correct sitemap URLs

Test plan

  • Verify /robots.txt returns correct content
  • Verify /sitemap.xml generates valid XML
  • Check metrics appear at /metrics
  • Submit sitemap to Google Search Console after deploy

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • /sitemap.xml and /robots.txt endpoints for search engines with caching.
    • Rich embedded quote/article/highlight rendering (author avatars, titles, summaries, draft badges, context).
    • Bech32 links now include relay hints when available.
  • Improvements

    • Better background profile metadata fetching to keep profiles up-to-date.
    • Unknown reference collection and non-fatal fetching to fill missing content.
  • Style

    • New CSS for embedded quotes, article cards, highlights, and draft badge.

@alltheseas

Copy link
Copy Markdown
Contributor Author

@coderabbitai review please

@coderabbitai

coderabbitai Bot commented Jan 16, 2026

Copy link
Copy Markdown

@alltheseas I'll review the changes in this PR for you.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jan 16, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds sitemap and robots.txt generation and routes; integrates sitemap generation with the database. Also introduces unknowns collection/fetching, bech32 relay-hinting, extended rendering for embedded quotes/highlights, and several RelayPool and render API changes.

Changes

Cohort / File(s) Summary
HTTP routing & server entry
src/main.rs
Wires new sitemap module and adds routes: GET /robots.txt (static robots with long cache) and GET /sitemap.xml (calls sitemap::generate_sitemap(&app.ndb); returns XML or 500). Removes RelayPool timeout arg from constructor call.
Sitemap module
src/sitemap.rs
New module: generate_sitemap(&Ndb) -> Result<String, nostrdb::Error> and generate_robots_txt() -> String. Builds sitemap from homepage, recent notes, evergreen articles, and profiles; includes base-URL normalization, XML escaping, ISO-like date formatting, simple date helpers, metrics, and tests.
Rendering, unknowns & fetching
src/render.rs, src/unknowns.rs, src/render.rs tests
Adds UnknownIds collection and public APIs to collect and represent missing note/profile IDs and relay hints. find_note now returns source relays; render data tracks source_relays; new fetch_unknowns and collect_note_unknowns integrate unknowns into fetch workflows and use relay hints for targeted fetches.
HTML rendering & embeds
src/html.rs
Large enhancements to note/article rendering: NIP‑84 highlight extraction, embedded quote/article cards, new helper types (QuoteRef, QuoteProfileInfo, HighlightMetadata), rendering helpers, and render_note_content signature changed to accept ndb and txn. Adds bech32_with_relays usage to include relay hints.
NIP19 handling
src/nip19.rs
Adds pub fn bech32_with_relays(nip19: &Nip19, source_relays: &[RelayUrl]) -> Option<String> to produce bech32 identifiers with relay hints; simplifies relay extraction from Nip19 variants and adds tests.
RelayPool & streaming
src/relay_pool.rs
Removes connect_timeout field and constructor parameter; new signature updated. Streaming API adjusted: stream_events now accepts a single Filter and returns BoxedStream<RelayEvent> (replaces ReceiverStream<Event>). Connect calls use client.connect().await.
Public API adjustments
src/render.rs, src/relay_pool.rs, src/html.rs, src/nip19.rs
Multiple public signature changes: RelayPool::new(...), RelayPool::stream_events(...), render_note_content(...), find_note(...) return type updated, and new public functions/types from sitemap/unknowns/nip19.
Deps, CI & assets
Cargo.toml, .github/workflows/rust.yml, assets/damus.css
Dependency source/version updates (nostrdb -> v0.9.0; nostr-sdk/nostr -> git branch), minor CI apt-get change, and large CSS additions for embedded quotes/highlights/article drafts.
sequenceDiagram
  participant Client as "Client (browser/crawler)"
  participant Server as "HTTP server (main.rs)"
  participant Sitemap as "sitemap module"
  participant Ndb as "Ndb (nostrdb)"

  Client->>Server: GET /sitemap.xml
  Server->>Sitemap: generate_sitemap(&app.ndb)
  Sitemap->>Ndb: query notes, articles, profiles
  Ndb-->>Sitemap: results
  Sitemap-->>Server: sitemap XML
  Server-->>Client: 200 application/xml (with cache headers)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I dug through tags and urls bright,
I gathered notes by day and night,
A sitemap nest, robots in tow,
So searchers find where breadcrumbs go —
Hop on, indexers, take flight! ✨

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR contains several significant out-of-scope changes beyond the stated sitemap/robots.txt objectives, including RelayPool API redesign, extensive HTML rendering enhancements, NIP-19 relay handling, and new unknowns collection module. Separate the SEO sitemap/robots.txt feature (sitemap.rs, main.rs routing, GitHub Actions, Cargo.toml, assets/damus.css) into a focused PR, moving RelayPool refactoring, HTML rendering, NIP-19 updates, and unknowns module to dedicated PRs with their own scope and objectives.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add sitemap.xml and robots.txt for SEO' clearly and concisely describes the main changes—adding sitemap and robots.txt functionality for search engine optimization.
Linked Issues check ✅ Passed The PR successfully implements the core coding requirement from issue #26: sitemap.xml generation with dynamic content from nostrdb cache, plus robots.txt serving, enabling search engine discovery and Google Search Console integration.
Docstring Coverage ✅ Passed Docstring coverage is 96.39% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into master

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/sitemap.rs`:
- Around line 269-276: Update the metrics calls to use the metrics 0.21.0 API:
replace the incorrect usage of counter!("sitemap_generations_total", 1) with
either the increment_counter! convenience macro or call
counter!("sitemap_generations_total").increment(1), and replace
gauge!("sitemap_generation_duration_seconds", duration.as_secs_f64()) and the
other gauge calls for "sitemap_urls_total", "sitemap_notes_count",
"sitemap_articles_count", and "sitemap_profiles_count" with
gauge("...").set(value) (or use a suitable gauge-set helper) so the macros
return handles before invoking .increment(...) or .set(...); use the existing
local variables start, duration (computed from start.elapsed()), entries.len(),
notes_count, articles_count, and profiles_count as the values passed to the
handle methods.
🧹 Nitpick comments (2)
src/sitemap.rs (2)

71-79: Redundant datetime conversion.

Lines 75-79 create a datetime from timestamp, then immediately extract the same value back as secs_since_epoch. This is unnecessary; you can use timestamp directly.

♻️ Suggested simplification
 fn format_lastmod(timestamp: u64) -> String {
-    use std::time::{Duration, UNIX_EPOCH};
-
-    let datetime = UNIX_EPOCH + Duration::from_secs(timestamp);
-    let secs_since_epoch = datetime
-        .duration_since(UNIX_EPOCH)
-        .unwrap_or_default()
-        .as_secs();
-
     // Simple date formatting without external dependencies
-    let days_since_epoch = secs_since_epoch / 86400;
+    let days_since_epoch = timestamp / 86400;
     let mut year = 1970i32;
     let mut remaining_days = days_since_epoch as i32;

160-160: Consider logging query failures for observability.

Using unwrap_or_default() provides resilience, but query failures are silently ignored. Consider logging a warning when queries fail to help diagnose issues in production. The same applies to lines 187 and 235.

♻️ Optional: Log query errors
-    let results = ndb.query(&txn, &[notes_filter], MAX_SITEMAP_URLS as i32).unwrap_or_default();
+    let results = ndb
+        .query(&txn, &[notes_filter], MAX_SITEMAP_URLS as i32)
+        .unwrap_or_else(|e| {
+            tracing::warn!("Failed to query notes for sitemap: {e}");
+            vec![]
+        });
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad9d797 and 34db03a.

📒 Files selected for processing (2)
  • src/main.rs
  • src/sitemap.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/main.rs (1)
src/sitemap.rs (2)
  • generate_robots_txt (282-294)
  • generate_sitemap (129-279)
🔇 Additional comments (10)
src/sitemap.rs (7)

1-22: LGTM!

Constants are well-documented and reasonable. Using OnceLock for lazy initialization of the base URL is appropriate for avoiding repeated environment variable lookups.


24-44: LGTM!

The lazy initialization pattern with a one-time warning for missing configuration is a good approach. The normalization correctly handles trailing slashes.


46-53: LGTM!

Good defensive coding with unwrap_or_default() and saturating_sub() to handle edge cases gracefully.


55-69: LGTM!

Correct XML escaping implementation covering all required special characters with appropriate capacity pre-allocation.


80-118: LGTM!

The date calculation logic is correct, including proper leap year handling with the standard formula.


281-294: LGTM!

The robots.txt output is well-structured with appropriate directives. The wildcard pattern /*.json is supported by major search engines (Google, Bing).


296-356: LGTM!

Good test coverage for the utility functions with meaningful test cases including edge cases like leap years.

src/main.rs (3)

36-36: LGTM!

Module declaration is correctly placed.


143-150: LGTM!

The route follows the established pattern for static content routes. The 24-hour cache duration is appropriate for robots.txt.


151-167: LGTM!

Good error handling pattern with logging before returning 500. The 1-hour cache is appropriate for dynamic content, and the XML content type is correct.

Note: generate_sitemap performs synchronous database queries, which is consistent with how other routes in this file use ndb. If sitemap generation becomes slow with larger databases, consider offloading to a blocking task with tokio::task::spawn_blocking.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread src/sitemap.rs
Comment on lines +269 to +276
// Record metrics (aggregate stats, not user-tracking)
let duration = start.elapsed();
metrics::counter!("sitemap_generations_total", 1);
metrics::gauge!("sitemap_generation_duration_seconds", duration.as_secs_f64());
metrics::gauge!("sitemap_urls_total", entries.len() as f64);
metrics::gauge!("sitemap_notes_count", notes_count as f64);
metrics::gauge!("sitemap_articles_count", articles_count as f64);
metrics::gauge!("sitemap_profiles_count", profiles_count as f64);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check for metrics crate version in Cargo.toml
find . -name "Cargo.toml" -type f | head -5 | xargs grep -l "metrics" 2>/dev/null

Repository: damus-io/notecrumbs

Length of output: 75


🏁 Script executed:

# Also verify the actual code at the specified lines
if [ -f "src/sitemap.rs" ]; then
  sed -n '269,276p' src/sitemap.rs
fi

Repository: damus-io/notecrumbs

Length of output: 565


🌐 Web query:

metrics crate Rust 0.21.0 counter macro syntax increment

💡 Result:

Use the counter! macro to get a Counter handle, then call increment(n); or use the convenience macro increment_counter! to add 1 (or pass labels). Examples:

  • Get handle and increment by n:
    let c = counter!("my_metric");
    c.increment(5);
    [1]

  • Increment by 1 directly:
    increment_counter!("my_metric");
    // or with labels:
    increment_counter!("my_metric", "service" => "http");
    [2]

Docs (macro reference and examples) show the counter! and increment_counter! usage. [1] [2]

Sources:
[1] docs.rs: metrics::counter! macro.
[2] docs.rs / metrics macro increment_counter! (examples).


🏁 Script executed:

# Read the Cargo.toml to find the metrics version
cat Cargo.toml | grep -A 2 "^\[dependencies\]" | head -20

Repository: damus-io/notecrumbs

Length of output: 147


🏁 Script executed:

# Or search more directly for metrics
grep "metrics" Cargo.toml

Repository: damus-io/notecrumbs

Length of output: 116


🌐 Web query:

metrics crate Rust gauge macro syntax 0.21

💡 Result:

The gauge! macro (as of metrics 0.21) accepts these forms:

  • gauge!( $name:expr $(, $label_key:expr $(=> $label_value:expr)? )* $(,)? )
  • gauge!( level: $level:expr, $name:expr $(, $label_key:expr $(=> $label_value:expr)? )* $(,)? )
  • gauge!( target: $target:expr, $name:expr $(, $label_key:expr $(=> $label_value:expr)? )* $(,)? )
  • gauge!( target: $target:expr, level: $level:expr, $name:expr $(, $label_key:expr $(=> $label_value:expr)? )* $(,)? )

Examples:

  • let g = gauge!("my_gauge");
  • let g = gauge!("my_gauge", "service" => "http");
  • let g = gauge!(level: Level::DEBUG, "my_gauge");
  • let g = gauge!(format!("{}{}", prefix, "g"), &labels);

(You get a Gauge handle which supports .increment(), .decrement(), .set().) (docs.rs)

If you want the docs page for that macro/version, see the metrics macro docs and the 0.21.0 changelog. (docs.rs)

Citations:


Fix metrics macro syntax for version 0.21.0.

The counter!() and gauge!() macros in metrics 0.21.0 return handles that require method calls. The current syntax is incorrect:

  • counter!("name", value) should be counter!("name").increment(value) (or use increment_counter!("name") convenience macro)
  • gauge!("name", value) should be gauge!("name").set(value)

All metrics calls in lines 269-276 need to be updated to use the correct API.

🤖 Prompt for AI Agents
In `@src/sitemap.rs` around lines 269 - 276, Update the metrics calls to use the
metrics 0.21.0 API: replace the incorrect usage of
counter!("sitemap_generations_total", 1) with either the increment_counter!
convenience macro or call counter!("sitemap_generations_total").increment(1),
and replace gauge!("sitemap_generation_duration_seconds",
duration.as_secs_f64()) and the other gauge calls for "sitemap_urls_total",
"sitemap_notes_count", "sitemap_articles_count", and "sitemap_profiles_count"
with gauge("...").set(value) (or use a suitable gauge-set helper) so the macros
return handles before invoking .increment(...) or .set(...); use the existing
local variables start, duration (computed from start.elapsed()), entries.len(),
notes_count, articles_count, and profiles_count as the values passed to the
handle methods.

jb55 and others added 18 commits February 4, 2026 20:19
Fixes 404 errors when Ubuntu package versions change on mirrors.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update to nostrdb 0.9.0 and fix breaking API changes:
- FilterBuilder.tags() now takes &str instead of String
- Handle new FilterField variants (Search, Relays, Custom)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
NIP-84 Highlights (kind:9802):
- Extract and render highlight metadata (context, comment)
- Source attribution for web URLs, notes, and articles
- Blockquote styling with left border accent

NIP-18 Embedded Quotes:
- Parse q tags and inline nevent/note/naddr mentions
- Rich quote cards with avatar, name, @handle, relative time
- Reply detection using nostrdb's NoteReply
- Type indicators for articles/highlights/drafts

Other improvements:
- Draft badge for unpublished articles (kind:30024)
- @username handles displayed under profile names
- Human-readable @mentions (resolve npub to display names)

Closes: damus-io#51
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: alltheseas <alltheseas@users.noreply.github.com>
Embedded article quotes now display as cards matching iOS Damus:
- Hero image (if available)
- Bold article title
- Summary text (if available)
- Word count
- DRAFT badge via CSS for kind 30024

Co-Authored-By: alltheseas <alltheseas@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add an UnknownIds pattern (adapted from notedeck) to fetch quoted events
referenced in q tags and inline mentions. Events are fetched using relay
hints from nevent/naddr bech32 and q tag relay fields.

- Add src/unknowns.rs with UnknownId enum and UnknownIds collection
- Update QuoteRef to include relay hints (Vec<RelayUrl>)
- Extract relay hints from nevent/naddr bech32 and q tag third element
- Add collect_quote_unknowns() and fetch_unknowns() to render.rs
- Fetch quote unknowns in main.rs after primary note is loaded

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Expand the unknowns pattern beyond just quoted events to collect:
- Author profiles
- Reply chain (root/reply) using nostrdb's NoteReply (NIP-10)
- Mentioned profiles (npub/nprofile with relay hints)
- Mentioned events (nevent/note1 with relay hints)
- Quoted events (q tags, inline mentions)

Move unknowns collection to main.rs for consistent handling
regardless of whether primary note was cached.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: William Casarin <jb55@jb55.com>
Previously, background profile refreshes only fetched kind 1 (notes),
never updating kind 0 (profile metadata). This caused profiles to remain
stale indefinitely after initial cache.

Now fetch_profile_feed also fetches the latest profile metadata from
relays, allowing nostrdb to update cached profiles with newer versions.

Fixes: damus-io#52

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: William Casarin <jb55@jb55.com>
…d_quotes_html

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Integrate relay provenance tracking from rust-nostr PR #1172 to enable
proper NIP-19 bech32 links with relay hints for better content discoverability.

Changes:
- Update nostr-sdk to alltheseas/rust-nostr relay-provenance-tracking branch
- RelayPool::stream_events returns BoxedStream<RelayEvent> with source relay URL
- NoteAndProfileRenderData stores source_relays captured during fetch
- Generate bech32 links with relay hints for all event types (notes, articles, highlights)
- Filter profile (kind 0) relays from note hints
- Prioritize default relays in source_relays for reliability
- Preserve author/kind fields when rebuilding nevent bech32
- Graceful fallback to original nip19 on encoding failure with metric
- Add bech32_with_relays() helper with 8 unit tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use process_event_with instead of process_event to pass the source
relay URL to nostrdb when ingesting events from relay streams.

Fixes: fffaa9e ("feat: integrate rust-nostr PR #1172 relay provenance tracking")
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add dynamic sitemap generation from nostrdb cache to improve search
engine discoverability of Nostr content.

New routes:
- GET /robots.txt - crawler directives with sitemap reference
- GET /sitemap.xml - dynamic sitemap from cached notes/profiles/articles

The sitemap queries local nostrdb for:
- Notes (kind:1) → note1xxx URLs
- Long-form articles (kind:30023) → naddr1xxx URLs
- Profiles (kind:0) → npub1xxx URLs

Ref: damus-io#26

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Track aggregate stats (privacy-preserving, no user tracking):
- sitemap_generations_total: counter for generation requests
- sitemap_generation_duration_seconds: time to generate
- sitemap_urls_total: total URLs in sitemap
- sitemap_notes_count: notes included
- sitemap_articles_count: articles included
- sitemap_profiles_count: profiles included

Metrics available at /metrics endpoint.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Skip kind:30023 entries with missing/empty d-tag to avoid ambiguous
  URLs and potential collisions across authors
- Add since filter (90 days) to notes and articles queries to prioritize
  recent content for SEO freshness
- Log warning when NOTECRUMBS_BASE_URL is not set, to surface potential
  misconfiguration in production

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Cache base URL with OnceLock to avoid logging warning on every request
- Use separate lookback periods: 90 days for notes, 365 days for
  evergreen article content (kind:30023)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use early returns and let-else patterns to reduce nesting depth in
generate_sitemap loops. Improves readability by making the happy path
linear instead of deeply indented.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main.rs (1)

502-502: ⚠️ Potential issue | 🔴 Critical

Same metrics syntax issue.

Line 502 uses the incorrect two-argument gauge!() form.

-            metrics::gauge!("relay_pool_known_relays", tracked as f64);
+            metrics::gauge!("relay_pool_known_relays").set(tracked as f64);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` at line 502, The gauge! macro is being used with the wrong
two-argument form; replace the current call
metrics::gauge!("relay_pool_known_relays", tracked as f64); with the correct
form that passes a single numeric value expression (cast to f64 if needed), e.g.
call metrics::gauge!("relay_pool_known_relays", tracked_value_as_f64) where
tracked_value_as_f64 is the tracked variable cast to f64 before/inside the macro
invocation; update the usage around the gauge! macro and the tracked identifier
accordingly.
src/relay_pool.rs (1)

63-63: ⚠️ Potential issue | 🔴 Critical

Metrics macro syntax is invalid for metrics 0.21. All counter!() and gauge!() calls use an unsupported two-argument form.

The macros counter!("name", value) and gauge!("name", value) are incorrect. The metrics 0.21 API registers metrics by returning a handle; numeric values must be passed via methods on that handle. This affects lines 63, 80, 100, 103, and 139.

Proposed fix for all occurrences
-        metrics::counter!("relay_pool_ensure_calls_total", 1);
+        metrics::counter!("relay_pool_ensure_calls_total").increment(1);
-            metrics::counter!("relay_pool_relays_added_total", relays_added);
+            metrics::counter!("relay_pool_relays_added_total").increment(relays_added);
-            metrics::counter!("relay_pool_connect_success_total", connect_success);
+            metrics::counter!("relay_pool_connect_success_total").increment(connect_success);
-            metrics::counter!("relay_pool_connect_failure_total", connect_failure);
+            metrics::counter!("relay_pool_connect_failure_total").increment(connect_failure);
-        metrics::gauge!("relay_pool_known_relays", tracked as f64);
+        metrics::gauge!("relay_pool_known_relays").set(tracked as f64);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/relay_pool.rs` at line 63, The metrics macro calls use the old
two-argument form; replace each two-argument call with a handle + method call:
call metrics::counter!("relay_pool_ensure_calls_total") to get the counter
handle and then call .increment(1) (or .add(1)) on it; do the same for the other
counters (relay_pool_check_ok_total, relay_pool_check_errors_total) and for
gauges call metrics::gauge!("relay_pool_up_gauge") and
metrics::gauge!("relay_pool_backoff_ms_gauge") to get gauge handles and then set
their values via .set(f64) (or appropriate update method). Update all
occurrences referenced (the counter/gauge invocations around the symbols
relay_pool_ensure_calls_total, relay_pool_check_ok_total,
relay_pool_check_errors_total, relay_pool_up_gauge, relay_pool_backoff_ms_gauge)
so no call uses the unsupported two-argument macro form.
🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.


In `@assets/damus.css`:
- Around line 503-660: The CSS repeats the draft-orange hex (`#ff6b35`) in
.damus-article-draft, .damus-embedded-quote-type-draft, and
.damus-embedded-article-title.damus-embedded-article-draft::after; extract this
value into a custom property (e.g. --damus-draft) in :root and replace the
hardcoded `#ff6b35` occurrences with var(--damus-draft) so the draft color is
centralized and consistent across .damus-article-draft,
.damus-embedded-quote-type-draft, and the ::after pseudo-element.

In `@Cargo.toml`:
- Around line 22-24: Replace the two git dependencies that point to the personal
fork with references to the upstream rust-nostr repository: update the nostr-sdk
and nostr entries in Cargo.toml to use the official rust-nostr repo (use the
appropriate upstream git URL or, preferably, a crate version constraint) and, if
you must keep a git dependency, pin it with rev = "..." for reproducible builds;
ensure you edit the dependency keys "nostr-sdk" and "nostr" to reference the
upstream source and/or a fixed version instead of
"https://github.com/alltheseas/rust-nostr.git".

In `@src/html.rs`:
- Line 438: Replace the direct unwrap on block.as_mention() with a safe guard
like the other blocks: use if let Some(mention) = block.as_mention() { ... } (or
match) and bail/continue early in the else branch so the code only uses the
mention variable when it is present; update the block handling that currently
does let mention = block.as_mention().unwrap() to follow the same guard pattern
used elsewhere for BlockType::MentionBech32.
- Line 2068: The metrics macro call uses the wrong syntax—replace the incorrect
metrics::counter!("bech32_encode_fallback_total", 1) with the proper increment
macro metrics::increment_counter!("bech32_encode_fallback_total") (or use
metrics::counter!("bech32_encode_fallback_total", 1.0) only if your metrics
crate expects a float value); update the invocation in src/html.rs and ensure
the metrics macros are imported/available so the new metrics::increment_counter!
call compiles.
- Around line 707-728: In lookup_article_by_addr, the use of the `?` on
`tag.get_str(0)?` will return None and abort the whole lookup if any tag lacks a
name; replace that with a non-failing check (e.g., use `if let Some(tag_name) =
tag.get_str(0) { ... } else { continue }`) so malformed tags are skipped rather
than propagating None, keeping the existing match arms for "d" and "title" and
preserving the logic that sets `found_d_match` and `title`.
- Around line 852-855: The fallback avatar URL used in the embedded quote avatar
unwrap_or_else returns an image src of "/img/no-profile.svg" which is not
served; update that fallback to a served asset (e.g. change the returned string
to use "/assets/default_pfp.jpg" or another existing asset) so embedded quote
avatars load correctly, locating the expression that builds the img tag in
src/html.rs (the unwrap_or_else returning the img tag) and replace the path
accordingly; alternatively, if you prefer server-side, add "/img/no-profile.svg"
to the static asset routes in main.rs so the existing path is served.

In `@src/main.rs`:
- Around line 217-225: Currently render::fetch_unknowns is awaited inline during
handling of RenderData::Note (after render::collect_note_unknowns), which adds
request latency; instead, call fetch_unknowns asynchronously and do not await it
on the request path. Spawn a background task (e.g., tokio::spawn) that takes
clones/handles of app.relay_pool and app.ndb and calls render::fetch_unknowns(…,
unknowns).await inside the task, and log any errors there—do not block the
current request; keep the existing collect_note_unknowns and unknowns handling
but replace the inline await with a detached task that handles errors locally.
- Line 502: The gauge! macro is being used with the wrong two-argument form;
replace the current call metrics::gauge!("relay_pool_known_relays", tracked as
f64); with the correct form that passes a single numeric value expression (cast
to f64 if needed), e.g. call metrics::gauge!("relay_pool_known_relays",
tracked_value_as_f64) where tracked_value_as_f64 is the tracked variable cast to
f64 before/inside the macro invocation; update the usage around the gauge! macro
and the tracked identifier accordingly.

In `@src/relay_pool.rs`:
- Line 63: The metrics macro calls use the old two-argument form; replace each
two-argument call with a handle + method call: call
metrics::counter!("relay_pool_ensure_calls_total") to get the counter handle and
then call .increment(1) (or .add(1)) on it; do the same for the other counters
(relay_pool_check_ok_total, relay_pool_check_errors_total) and for gauges call
metrics::gauge!("relay_pool_up_gauge") and
metrics::gauge!("relay_pool_backoff_ms_gauge") to get gauge handles and then set
their values via .set(f64) (or appropriate update method). Update all
occurrences referenced (the counter/gauge invocations around the symbols
relay_pool_ensure_calls_total, relay_pool_check_ok_total,
relay_pool_check_errors_total, relay_pool_up_gauge, relay_pool_backoff_ms_gauge)
so no call uses the unsupported two-argument macro form.

In `@src/sitemap.rs`:
- Around line 254-267: The sitemap builder writes base_url directly into the
XML, so unescaped characters in NOTECRUMBS_BASE_URL can break the sitemap;
update the code that constructs the loc value (either where SitemapEntry is
built or in this loop that writes entries) to XML-escape the entire loc string
(not just the bech32 portion) before writing it into xml—e.g., apply the
existing xml_escape function (or equivalent) to entry.loc or escape base_url at
initialization so that the final loc written by the loop is always XML-safe.
- Around line 152-177: The sitemap is regenerated synchronously on each cache
miss in generate_sitemap and should be server-cached to avoid blocking HTTP
handlers; add a shared in-memory cache (e.g., Mutex<Option<(std::time::Instant,
String)>> or RwLock) accessible to the sitemap handler and modify
generate_sitemap to first check the cache age and return the cached XML if
fresh, otherwise regenerate the XML, then update the cache with the new String
and timestamp; use double-checked locking so you don't hold the lock while
calling heavy functions like ndb.query, and reference generate_sitemap,
ndb.query, MAX_SITEMAP_URLS, and NOTES_LOOKBACK_DAYS when locating where to add
the cache logic.

In `@src/unknowns.rs`:
- Around line 233-251: When ndb.get_note_by_id(txn, ev.id()) returns Err we
currently call add_note_if_missing which performs the same lookup again;
instead, avoid the redundant lookup by directly recording the missing event
(e.g. insert the event id into the unknowns map) and extend relays there, then
still handle the optional author lookup: keep the check using ev.pubkey() and
insert UnknownId::Profile via self.ids.entry(...).or_default().extend(relays).
Update the Mention::Event branch to perform the direct insert for the missing
event rather than calling add_note_if_missing (or refactor add_note_if_missing
to accept a fast-path that skips the second ndb.get_note_by_id).
🧹 Nitpick comments (4)
🤖 Fix all nitpicks with AI agents
Verify each finding against the current code and only fix it if needed.


In `@assets/damus.css`:
- Around line 503-660: The CSS repeats the draft-orange hex (`#ff6b35`) in
.damus-article-draft, .damus-embedded-quote-type-draft, and
.damus-embedded-article-title.damus-embedded-article-draft::after; extract this
value into a custom property (e.g. --damus-draft) in :root and replace the
hardcoded `#ff6b35` occurrences with var(--damus-draft) so the draft color is
centralized and consistent across .damus-article-draft,
.damus-embedded-quote-type-draft, and the ::after pseudo-element.

In `@src/main.rs`:
- Around line 217-225: Currently render::fetch_unknowns is awaited inline during
handling of RenderData::Note (after render::collect_note_unknowns), which adds
request latency; instead, call fetch_unknowns asynchronously and do not await it
on the request path. Spawn a background task (e.g., tokio::spawn) that takes
clones/handles of app.relay_pool and app.ndb and calls render::fetch_unknowns(…,
unknowns).await inside the task, and log any errors there—do not block the
current request; keep the existing collect_note_unknowns and unknowns handling
but replace the inline await with a detached task that handles errors locally.

In `@src/sitemap.rs`:
- Around line 152-177: The sitemap is regenerated synchronously on each cache
miss in generate_sitemap and should be server-cached to avoid blocking HTTP
handlers; add a shared in-memory cache (e.g., Mutex<Option<(std::time::Instant,
String)>> or RwLock) accessible to the sitemap handler and modify
generate_sitemap to first check the cache age and return the cached XML if
fresh, otherwise regenerate the XML, then update the cache with the new String
and timestamp; use double-checked locking so you don't hold the lock while
calling heavy functions like ndb.query, and reference generate_sitemap,
ndb.query, MAX_SITEMAP_URLS, and NOTES_LOOKBACK_DAYS when locating where to add
the cache logic.

In `@src/unknowns.rs`:
- Around line 233-251: When ndb.get_note_by_id(txn, ev.id()) returns Err we
currently call add_note_if_missing which performs the same lookup again;
instead, avoid the redundant lookup by directly recording the missing event
(e.g. insert the event id into the unknowns map) and extend relays there, then
still handle the optional author lookup: keep the check using ev.pubkey() and
insert UnknownId::Profile via self.ids.entry(...).or_default().extend(relays).
Update the Mention::Event branch to perform the direct insert for the missing
event rather than calling add_note_if_missing (or refactor add_note_if_missing
to accept a fast-path that skips the second ndb.get_note_by_id).
assets/damus.css (1)

503-660: Consider extracting the repeated draft-orange color into a CSS variable.

The draft color #ff6b35 appears in four places across this file (lines 360, 571–572, 617) — in .damus-article-draft, .damus-embedded-quote-type-draft, and the ::after pseudo-element. Extracting it to a --damus-draft custom property in :root would make future palette changes easier and keep the pattern consistent with the rest of the design system.

Otherwise the embedded-quote and article-card blocks look clean and consistent.

♻️ Suggested extraction

Add to :root:

 :root {
   /* ... existing vars ... */
+  --damus-draft: `#ff6b35`;
+  --damus-draft-end: `#f7931a`;
 }

Then replace hardcoded values, e.g.:

 .damus-article-draft {
-  background: linear-gradient(135deg, `#ff6b35`, `#f7931a`);
+  background: linear-gradient(135deg, var(--damus-draft), var(--damus-draft-end));
 .damus-embedded-quote-type-draft {
-  background: rgba(255, 107, 53, 0.2);
-  color: `#ff6b35`;
+  background: rgba(255, 107, 53, 0.2);
+  color: var(--damus-draft);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/damus.css` around lines 503 - 660, The CSS repeats the draft-orange
hex (`#ff6b35`) in .damus-article-draft, .damus-embedded-quote-type-draft, and
.damus-embedded-article-title.damus-embedded-article-draft::after; extract this
value into a custom property (e.g. --damus-draft) in :root and replace the
hardcoded `#ff6b35` occurrences with var(--damus-draft) so the draft color is
centralized and consistent across .damus-article-draft,
.damus-embedded-quote-type-draft, and the ::after pseudo-element.
src/main.rs (1)

217-225: Unknowns fetch adds latency to every note request.

fetch_unknowns is awaited inline on the request path (bounded by a 1.5s timeout per filter). For notes with many quoted events/profiles, this could add noticeable latency. This is acceptable for now since it improves rendering quality, but consider making this non-blocking in a future iteration (render with what's available, then fetch unknowns asynchronously for subsequent requests).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 217 - 225, Currently render::fetch_unknowns is
awaited inline during handling of RenderData::Note (after
render::collect_note_unknowns), which adds request latency; instead, call
fetch_unknowns asynchronously and do not await it on the request path. Spawn a
background task (e.g., tokio::spawn) that takes clones/handles of app.relay_pool
and app.ndb and calls render::fetch_unknowns(…, unknowns).await inside the task,
and log any errors there—do not block the current request; keep the existing
collect_note_unknowns and unknowns handling but replace the inline await with a
detached task that handles errors locally.
src/sitemap.rs (1)

152-177: Sitemap generation is synchronous on the request thread.

generate_sitemap performs three potentially large NDB queries (up to 10k results each) synchronously. On a hot path (every sitemap request that misses the 1-hour cache), this blocks the HTTP handler.

This is mitigated by the max-age=3600 cache header in main.rs, but the cache is client-side only — there's no server-side caching, so every unique client triggers a full regeneration. Consider caching the generated XML in memory (e.g., behind a Mutex<Option<(Instant, String)>>) to avoid redundant work under load.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sitemap.rs` around lines 152 - 177, The sitemap is regenerated
synchronously on each cache miss in generate_sitemap and should be server-cached
to avoid blocking HTTP handlers; add a shared in-memory cache (e.g.,
Mutex<Option<(std::time::Instant, String)>> or RwLock) accessible to the sitemap
handler and modify generate_sitemap to first check the cache age and return the
cached XML if fresh, otherwise regenerate the XML, then update the cache with
the new String and timestamp; use double-checked locking so you don't hold the
lock while calling heavy functions like ndb.query, and reference
generate_sitemap, ndb.query, MAX_SITEMAP_URLS, and NOTES_LOOKBACK_DAYS when
locating where to add the cache logic.
src/unknowns.rs (1)

233-251: Minor: redundant ndb lookup in Mention::Event handling.

At Line 233, ndb.get_note_by_id(txn, ev.id()) is called. When it returns Err, Line 236 calls add_note_if_missing which calls ndb.get_note_by_id again internally. You could use a direct insert here to skip the second lookup.

Optional optimization
                 Mention::Event(ev) => {
                     let relays: HashSet<RelayUrl> = ev
                         .relays_iter()
                         .filter_map(|s| RelayUrl::parse(s).ok())
                         .collect();
 
                     match ndb.get_note_by_id(txn, ev.id()) {
                         Err(_) => {
-                            // Event not found - add it and its author if specified
-                            self.add_note_if_missing(ndb, txn, ev.id(), relays.clone());
+                            // Event not found - add directly (already checked)
+                            let unknown_id = UnknownId::NoteId(*ev.id());
+                            self.ids.entry(unknown_id).or_default().extend(relays.clone());
                             if let Some(pk) = ev.pubkey() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/unknowns.rs` around lines 233 - 251, When ndb.get_note_by_id(txn,
ev.id()) returns Err we currently call add_note_if_missing which performs the
same lookup again; instead, avoid the redundant lookup by directly recording the
missing event (e.g. insert the event id into the unknowns map) and extend relays
there, then still handle the optional author lookup: keep the check using
ev.pubkey() and insert UnknownId::Profile via
self.ids.entry(...).or_default().extend(relays). Update the Mention::Event
branch to perform the direct insert for the missing event rather than calling
add_note_if_missing (or refactor add_note_if_missing to accept a fast-path that
skips the second ndb.get_note_by_id).

Comment thread Cargo.toml
Comment on lines +22 to +24
# PR #1172: relay provenance tracking - https://github.com/rust-nostr/nostr/pull/1172
nostr-sdk = { git = "https://github.com/alltheseas/rust-nostr.git", branch = "relay-provenance-tracking" }
nostr = { git = "https://github.com/alltheseas/rust-nostr.git", branch = "relay-provenance-tracking" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

rust-nostr PR 1172 relay provenance tracking

💡 Result:

PR #1172 in rust-nostr/nostr (“Add relay provenance tracking to streaming APIs”, opened/updated Dec 18–19, 2025, now closed) adds relay provenance to the SDK’s event streaming interfaces by introducing a new RelayEvent wrapper and parallel “with source” streaming methods. (github.com)

What it changes

  • Adds a new type RelayEvent = an Event bundled with the relay URL(s) where it was seen (tracks Vec<RelayUrl> and provides accessors like relay_url() / relay_urls(), plus conversion helpers like into_event() / into_parts()). (github.com)
  • Adds new non-breaking, opt-in streaming APIs alongside the existing ones:
    • Client::stream_events_with_source*
    • RelayPool::stream_events_with_source* (including variants like *_from_* and *_targeted_*)
    • Relay::stream_events_with_source
    • and exports RelayEvent from nostr-sdk. (github.com)

Why it exists (the “provenance” use case)

The main goal is to let downstream apps know which relay delivered each streamed event, enabling:

  • Proper NIP-19 identifiers (nevent, nprofile) that include relay hints (so shared links are more discoverable). (github.com)
  • Better debugging/analytics and support for routing models like NIP-65 (outbox/inbox decisions based on where content is actually seen). (github.com)

Citations:


Migrate back to the upstream rust-nostr repository; PR #1172 is merged.

PR #1172 ("Add relay provenance tracking to streaming APIs") is now closed and merged into the official rust-nostr repo. There is no reason to depend on the personal fork alltheseas/rust-nostr any longer—it should create ongoing supply chain risk and maintenance burden.

Update both dependencies to use the official repositories instead. If you need a specific version, use a version constraint (= "0.x.y") or, if using a git dependency temporarily, always pin to a specific revision with rev = "..." for reproducible builds.

Example migration
-nostr-sdk = { git = "https://github.com/alltheseas/rust-nostr.git", branch = "relay-provenance-tracking" }
-nostr = { git = "https://github.com/alltheseas/rust-nostr.git", branch = "relay-provenance-tracking" }
+nostr-sdk = { version = "0.x.y" }
+nostr = { version = "0.x.y" }

(or with git + rev if a git dependency is required):

+nostr-sdk = { git = "https://github.com/rust-nostr/nostr", rev = "<commit-sha>" }
+nostr = { git = "https://github.com/rust-nostr/nostr", rev = "<commit-sha>" }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` around lines 22 - 24, Replace the two git dependencies that point
to the personal fork with references to the upstream rust-nostr repository:
update the nostr-sdk and nostr entries in Cargo.toml to use the official
rust-nostr repo (use the appropriate upstream git URL or, preferably, a crate
version constraint) and, if you must keep a git dependency, pin it with rev =
"..." for reproducible builds; ensure you edit the dependency keys "nostr-sdk"
and "nostr" to reference the upstream source and/or a fixed version instead of
"https://github.com/alltheseas/rust-nostr.git".

Comment thread src/html.rs
block.as_str(),
&abbrev_str(block.as_str())
);
let mention = block.as_mention().unwrap();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

unwrap() on block.as_mention() can panic.

While as_mention() should always succeed for BlockType::MentionBech32, an unwrap() is unnecessarily risky. Use the same guard pattern used elsewhere in this file (e.g., Line 506).

Proposed fix
             BlockType::MentionBech32 => {
-                let mention = block.as_mention().unwrap();
+                let Some(mention) = block.as_mention() else {
+                    continue;
+                };
                 let pubkey = match mention {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let mention = block.as_mention().unwrap();
let Some(mention) = block.as_mention() else {
continue;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/html.rs` at line 438, Replace the direct unwrap on block.as_mention()
with a safe guard like the other blocks: use if let Some(mention) =
block.as_mention() { ... } (or match) and bail/continue early in the else branch
so the code only uses the mention variable when it is present; update the block
handling that currently does let mention = block.as_mention().unwrap() to follow
the same guard pattern used elsewhere for BlockType::MentionBech32.

Comment thread src/html.rs
Comment on lines +707 to +728
for result in results {
let mut found_d_match = false;
let mut title = None;

for tag in result.note.tags() {
let tag_name = tag.get_str(0)?;
match tag_name {
"d" => {
if tag.get_str(1) == Some(d_identifier) {
found_d_match = true;
}
}
"title" => {
if let Some(t) = tag.get_str(1) {
if !t.trim().is_empty() {
title = Some(t.to_owned());
}
}
}
_ => {}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bug: ? operator in lookup_article_by_addr aborts the entire lookup if any tag lacks a name.

Line 712 uses let tag_name = tag.get_str(0)?; inside a for tag in result.note.tags() loop. Since the function returns Option<...>, the ? will cause the function to return None if any single tag in the note has no string at index 0 — skipping all remaining tags and results.

This should use continue to skip malformed tags instead:

Proposed fix
         for tag in result.note.tags() {
-            let tag_name = tag.get_str(0)?;
+            let Some(tag_name) = tag.get_str(0) else {
+                continue;
+            };
             match tag_name {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for result in results {
let mut found_d_match = false;
let mut title = None;
for tag in result.note.tags() {
let tag_name = tag.get_str(0)?;
match tag_name {
"d" => {
if tag.get_str(1) == Some(d_identifier) {
found_d_match = true;
}
}
"title" => {
if let Some(t) = tag.get_str(1) {
if !t.trim().is_empty() {
title = Some(t.to_owned());
}
}
}
_ => {}
}
}
for result in results {
let mut found_d_match = false;
let mut title = None;
for tag in result.note.tags() {
let Some(tag_name) = tag.get_str(0) else {
continue;
};
match tag_name {
"d" => {
if tag.get_str(1) == Some(d_identifier) {
found_d_match = true;
}
}
"title" => {
if let Some(t) = tag.get_str(1) {
if !t.trim().is_empty() {
title = Some(t.to_owned());
}
}
}
_ => {}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/html.rs` around lines 707 - 728, In lookup_article_by_addr, the use of
the `?` on `tag.get_str(0)?` will return None and abort the whole lookup if any
tag lacks a name; replace that with a non-failing check (e.g., use `if let
Some(tag_name) = tag.get_str(0) { ... } else { continue }`) so malformed tags
are skipped rather than propagating None, keeping the existing match arms for
"d" and "title" and preserving the logic that sets `found_d_match` and `title`.

Comment thread src/html.rs
Comment on lines +852 to +855
.unwrap_or_else(|| {
r#"<img src="/img/no-profile.svg" class="damus-embedded-quote-avatar" alt="" />"#
.to_string()
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if /img/no-profile.svg is served anywhere or exists as a static file
rg -n "no-profile" --type rust
fd "no-profile"

Repository: damus-io/notecrumbs

Length of output: 339


🏁 Script executed:

#!/bin/bash
# Check main.rs for static file serving configuration
rg -n "/img|/assets" src/main.rs
rg -n "no-profile" src/main.rs
# Look for static file configuration or route setup
rg -n "static|assets" src/main.rs -i

Repository: damus-io/notecrumbs

Length of output: 1097


🏁 Script executed:

#!/bin/bash
# Check context around the no-profile references
echo "=== Context around line 853 ==="
sed -n '840,865p' src/html.rs

echo -e "\n=== Context around line 1541 ==="
sed -n '1530,1550p' src/html.rs

echo -e "\n=== Context around line 1605 ==="
sed -n '1595,1615p' src/html.rs

# Check for any default route or catch-all
echo -e "\n=== Checking for catch-all or default routes ==="
rg -n "default|catch.*all|\*" src/main.rs | head -20

Repository: damus-io/notecrumbs

Length of output: 3965


🏁 Script executed:

#!/bin/bash
# Find get_base_url function and /img handling
rg -n "fn get_base_url|get_base_url()" src/
rg -n '"/img' src/main.rs
# Check if there's any wildcard or catch-all route
sed -n '100,150p' src/main.rs

Repository: damus-io/notecrumbs

Length of output: 2662


🏁 Script executed:

#!/bin/bash
# Check get_base_url() implementations
echo "=== get_base_url in sitemap.rs ==="
sed -n '26,35p' src/sitemap.rs

echo -e "\n=== get_base_url in html.rs ==="
sed -n '2018,2030p' src/html.rs

# Check full serve function for /img route
echo -e "\n=== Searching for /img in serve function ==="
rg -n '"/img' src/main.rs

# Check if there's a catch-all route at the end of serve
echo -e "\n=== End of serve function path matching ==="
rg -n 'path\(\)' src/main.rs -A 5

Repository: damus-io/notecrumbs

Length of output: 2242


🏁 Script executed:

#!/bin/bash
# Verify if the file actually exists in assets directory
ls -la assets/ 2>/dev/null | grep -i "no-profile\|img"

# Double-check: search for any /img route definition
rg -n '"/img' src/

# Check entire serve function to ensure we didn't miss anything
wc -l src/main.rs

Repository: damus-io/notecrumbs

Length of output: 192


Fix fallback avatar path in embedded quote avatars.

The fallback avatar at line 853 references /img/no-profile.svg, but this path is not served by the application. The server only serves static assets from /assets/ and specific routes like /damus.css and /fonts/. Embedded quote avatars without a profile picture URL will display broken images.

Change the path to point to a served asset, such as /assets/default_pfp.jpg, or add /img/no-profile.svg to the static asset routes in main.rs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/html.rs` around lines 852 - 855, The fallback avatar URL used in the
embedded quote avatar unwrap_or_else returns an image src of
"/img/no-profile.svg" which is not served; update that fallback to a served
asset (e.g. change the returned string to use "/assets/default_pfp.jpg" or
another existing asset) so embedded quote avatars load correctly, locating the
expression that builds the img tag in src/html.rs (the unwrap_or_else returning
the img tag) and replace the path accordingly; alternatively, if you prefer
server-side, add "/img/no-profile.svg" to the static asset routes in main.rs so
the existing path is served.

Comment thread src/html.rs
"failed to encode bech32 with relays for nip19: {:?}, falling back to original",
nip19
);
metrics::counter!("bech32_encode_fallback_total", 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Same incorrect metrics macro syntax.

-            metrics::counter!("bech32_encode_fallback_total", 1);
+            metrics::counter!("bech32_encode_fallback_total").increment(1);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
metrics::counter!("bech32_encode_fallback_total", 1);
metrics::counter!("bech32_encode_fallback_total").increment(1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/html.rs` at line 2068, The metrics macro call uses the wrong
syntax—replace the incorrect metrics::counter!("bech32_encode_fallback_total",
1) with the proper increment macro
metrics::increment_counter!("bech32_encode_fallback_total") (or use
metrics::counter!("bech32_encode_fallback_total", 1.0) only if your metrics
crate expects a float value); update the invocation in src/html.rs and ensure
the metrics macros are imported/available so the new metrics::increment_counter!
call compiles.

Comment thread src/sitemap.rs
Comment on lines +254 to +267
// Build XML
let mut xml = String::with_capacity(entries.len() * 200);
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xml.push_str("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");

for entry in &entries {
let _ = write!(
xml,
" <url>\n <loc>{}</loc>\n <lastmod>{}</lastmod>\n <changefreq>{}</changefreq>\n <priority>{}</priority>\n </url>\n",
entry.loc, entry.lastmod, entry.changefreq, entry.priority
);
}

xml.push_str("</urlset>\n");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

base_url is not XML-escaped in sitemap output.

On Line 256–257, base_url is written directly into the XML <urlset> output. While the loc bech32 portion is escaped via xml_escape, base_url itself (from env var) is never escaped. If someone sets NOTECRUMBS_BASE_URL to a value containing & or other XML-special characters, the sitemap XML will be malformed.

Proposed fix — escape loc as a whole, or escape base_url at init
 fn normalize_base_url(url: &str) -> String {
-    url.trim_end_matches('/').to_string()
+    let trimmed = url.trim_end_matches('/');
+    xml_escape(trimmed)
 }

Alternatively, escape the entire loc value when building SitemapEntry:

         entries.push(SitemapEntry {
-            loc: format!("{}/{}", base_url, xml_escape(&bech32)),
+            loc: xml_escape(&format!("{}/{}", base_url, bech32)),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sitemap.rs` around lines 254 - 267, The sitemap builder writes base_url
directly into the XML, so unescaped characters in NOTECRUMBS_BASE_URL can break
the sitemap; update the code that constructs the loc value (either where
SitemapEntry is built or in this loop that writes entries) to XML-escape the
entire loc string (not just the bech32 portion) before writing it into xml—e.g.,
apply the existing xml_escape function (or equivalent) to entry.loc or escape
base_url at initialization so that the final loc written by the loop is always
XML-safe.

@jb55
jb55 merged commit f773217 into damus-io:master Feb 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SEO: sitemap.xml

2 participants