Commit Graph

2986 Commits

Author SHA1 Message Date
Frédéric Guillot 15505142fd refactor(api): move entryIDsResponse struct to messages.go
Keep all API message types together in messages.go.
2026-06-22 15:58:43 -07:00
Frédéric Guillot 6ddddbc4c2 fix(api): remove duplicate category filter in entry list
The entry list path applied the category_id query parameter twice: once
with validation in findEntries and again without validation in
configureFilters. Drop the unvalidated second application so an invalid
category_id consistently returns a bad request.
2026-06-22 15:24:19 -07:00
Frédéric Guillot 063e3f14f0 refactor(api): parse counts parameter with QueryBoolParam
The categories handler parsed the counts query parameter by comparing
the raw string to "true". Use QueryBoolParam for consistency with the
other boolean query parameters in the package.
2026-06-22 15:20:19 -07:00
Frédéric Guillot ae1f1351bf fix(api): return server error on user lookup failure
The admin user lookup handlers reported a database error from UserByID
and UserByUsername as a bad request with a generic message. Return a
server error instead, consistent with the other user handlers.
2026-06-22 15:14:25 -07:00
Frédéric Guillot 5de36b613c fix(api): return server error when feed lookup fails on update
The update-feed handler returned a not found response for any error
from FeedByID, masking genuine database failures. Return a server error
on failure and keep the not found response for a nil feed.
2026-06-22 15:12:00 -07:00
Frédéric Guillot 2ac3073583 fix(api): check for nil user before using the result
The current-user and categories handlers used the value returned by
UserByID without checking whether it was nil. Since UserByID returns no
error when the user does not exist, the categories handler could
dereference a nil user. Return a not found response when the user is nil.
2026-06-22 15:05:52 -07:00
Frédéric Guillot ae6ac73ce4 fix(api): distinguish missing user from database error
The mark-user-as-read and integrations-status handlers treated any
error from UserByID as a 404, which masked genuine database failures
and never detected a missing user since UserByID returns no error when
the user does not exist. Return a server error on failure and a not
found response only when the user is nil.
2026-06-22 14:57:27 -07:00
Frédéric Guillot 9f4b2ef9fc fix(api): proxify enclosure URLs in entry list endpoints
The single-entry endpoint proxified enclosure URLs while the list
endpoints did not, so the same enclosure was returned with different
URLs depending on the endpoint used.
2026-06-22 14:49:15 -07:00
John Brayton bcb2cf2aa2 feat(api): Allow API client to set "starred" to true or false using the "PUT /v1/entries" endpoint 2026-06-19 17:47:32 -07:00
CULT PONY 530b0c5739 fix(urllib): fix rejection of valid proxy URLs in feeds
When attempting to set a proxy in a feed, due to a regression caused by 4cd9dd6af7
the form validation would now only accept HTTP(S) proxies, and reject SOCKS proxies, despite this being
a valid configuration before. Adding a new function to validate proxy URLs separates the concerns
and allows using a SOCKS proxy url again.
2026-06-16 20:30:19 -07:00
Cthulhux b0d25c1c68 fix(locale): de_DE updates 2026-06-16 14:07:54 -07:00
Frédéric Guillot 8699a1d119 fix(fetcher): clone request builder before disabling redirects
The subscription finder holds a shared RequestBuilder whose methods mutate
in place. Disabling redirects while probing well-known feed URLs flipped
that flag on the shared builder permanently, leaking into the finder's
other requests instead of scoping to the probe.

Add a Clone method and derive an isolated builder for the probe so the
redirect setting no longer escapes the loop.
2026-06-15 21:18:19 -07:00
Frédéric Guillot a1539659f3 fix(ui): show flash message when refreshing all feeds via keyboard
The R keyboard shortcut used a fetch request that followed the redirect
to /feeds automatically, consuming the one-time flash message before the
browser navigated there a second time, so no success alert appeared.

Submit a real form POST instead so the browser follows the redirect once
and renders the flash message, matching the menu button behavior.

Fixes: #4358
2026-06-15 20:42:01 -07:00
Frédéric Guillot 1975bc6339 fix(storage): scope enclosure lookup by user 2026-06-14 21:27:47 -07:00
Frédéric Guillot f72957e807 chore(version): bump dev version to 2.3.x-dev 2026-06-14 20:53:52 -07:00
viq ce5d84e956 feat(search): swap plainto_tsquery for websearch_to_tsquery
This switches plainto_tsquery to instead use websearch_to_tsquery,
introduced in PostgreSQL 11. With unquoted text it behaves the same, but
allows to use quoted text and OR and negation in the search terms.
2026-06-12 21:29:24 -07:00
gudvinr 49d8f1dafb refactor(response): make response builder actually build stuff 2026-06-12 21:20:53 -07:00
gudvinr 91860edab0 refactor(fetcher): make request builder actually build 2026-06-12 21:20:53 -07:00
gudvinr e49c75fc56 fix(storage): chain query builders
Correct use of builder assumes that each step makes isolated instance. Thus, not using result of build step makes builder just ignore that action.
2026-06-12 21:20:53 -07:00
gudvinr a14a180313 refactor(reader): use consistent naming for atom feed adapters 2026-06-12 21:14:16 -07:00
gudvinr 684ea3d224 feat(reader): split common sorting routine into separate function
Instead of creating unsorted slice and then sorting, just make slice sorted.
2026-06-12 21:14:16 -07:00
gudvinr 4ce8151a8f refactor(reader): make code flow consistent for buildFeed
Every build feed method does similar things in a different way. That makes it harder to read these implementations.

Remove nesting creep in loops to make code simpler.
2026-06-12 21:14:16 -07:00
John Brayton 7d8ffd2eb0 feat(api): add entry ID listing endpoint
Add GET /v1/entries/ids to return paginated entry IDs for the current user.

The endpoint supports status and starred filters, returns the total matching 
count, and exposes matching client methods.
2026-06-11 21:19:00 -07:00
dependabot[bot] 05cd8da81f build(deps): bump the gomod group with 5 updates
Bumps the gomod group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.52.0` | `0.53.0` |
| [golang.org/x/image](https://github.com/golang/image) | `0.41.0` | `0.42.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.55.0` | `0.56.0` |
| [golang.org/x/term](https://github.com/golang/term) | `0.43.0` | `0.44.0` |
| [golang.org/x/text](https://github.com/golang/text) | `0.37.0` | `0.38.0` |


Updates `golang.org/x/crypto` from 0.52.0 to 0.53.0
- [Commits](https://github.com/golang/crypto/compare/v0.52.0...v0.53.0)

Updates `golang.org/x/image` from 0.41.0 to 0.42.0
- [Commits](https://github.com/golang/image/compare/v0.41.0...v0.42.0)

Updates `golang.org/x/net` from 0.55.0 to 0.56.0
- [Commits](https://github.com/golang/net/compare/v0.55.0...v0.56.0)

Updates `golang.org/x/term` from 0.43.0 to 0.44.0
- [Commits](https://github.com/golang/term/compare/v0.43.0...v0.44.0)

Updates `golang.org/x/text` from 0.37.0 to 0.38.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
- dependency-name: golang.org/x/image
  dependency-version: 0.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
- dependency-name: golang.org/x/net
  dependency-version: 0.56.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
- dependency-name: golang.org/x/term
  dependency-version: 0.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
- dependency-name: golang.org/x/text
  dependency-version: 0.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-10 20:23:42 -07:00
jvoisin f22c08832a fix(migrations): use SHA-256 instead of MD5 for the enclosures unique index
PostgreSQL 18 disables MD5 when running in FIPS mode, which made
md5(url) unusable. This broke enclosure creation with:

    store: unable to create enclosure: pq: could not compute MD5
    hash: unsupported (XX000)

Replace md5(url) with encode(sha256(url::bytea), 'hex') everywhere:

- The historical migrations that created the enclosures index are
  changed to sha256 so fresh installs no longer fail while replaying
  them on FIPS-mode PostgreSQL 18.
- A new migration rebuilds enclosures_user_entry_url_unique_idx with
  sha256 to convert existing installs.
- The ON CONFLICT clause in createEnclosure is updated to match the
  new expression index.

According to `openssl speed -bytes 256 md5 sha256`, this is a performance
improvement as well :D

Finally, the PostgreSQL minimum version was bumped from 9.5 to 11, the lowest
version to support SHA256.

Fixes #4350
2026-06-09 21:00:27 -07:00
jvoisin f4de355374 fix: correct an aria-labelledby error 2026-06-08 21:03:38 -07:00
gudvinr f963ea35a5 refactor(cli): use cleaner syntax for common exit routine 2026-06-06 20:20:52 -07:00
jvoisin 87d7891600 refactor: remove the now-useless maxDepth limit in the sanitizer
As stated in html.Parse's documentation, "Parse will reject HTML that is nested
deeper than 512 elements." So there is no need to do it ourself.
2026-06-05 20:34:00 -07:00
ghose 939698fd66 feat(locale): update gl_ES 2026-06-05 17:26:55 -07:00
jiasen 350df63d99 fix(googlereader): apply configured user agent in quickadd handler
The /reader/api/0/subscription/quickadd endpoint was creating its
request builder without calling WithUserAgent, so outbound feed
fetches used Go's default user agent (Go-http-client/2.x) instead
of the operator-configured HTTP_CLIENT_USER_AGENT.

Sites like Reddit that block Go's default user agent would return
403, causing quickadd to fail even though adding the same feed via
the Miniflux web UI succeeded (the UI's subscription handler correctly
sets the configured user agent).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 21:14:45 -07:00
Frédéric Guillot 0cfd0798b9 fix(locale): drop unused arguments when formatting translations
Translation forms that intentionally omit the count, such as the Arabic
dual, no longer render a trailing %!(EXTRA ...) marker. The printer now
formats with the supplied arguments only when the string has a real
directive, and skips them otherwise while still unescaping percents.

This also fixes the same issue in the Polish and Romanian one-forms, and
makes the missing-translation fallback return the bare key.
2026-06-04 20:56:24 -07:00
dependabot[bot] f4b393748d build(deps): bump the github-actions group with 9 updates
Bumps the github-actions group with 9 updates:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `6.0.2` | `6.0.3` |
| [github/codeql-action](https://github.com/github/codeql-action) | `4.35.3` | `4.36.1` |
| [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) | `4.0.0` | `4.1.0` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `4.0.0` | `4.1.0` |
| [docker/metadata-action](https://github.com/docker/metadata-action) | `6.0.0` | `6.1.0` |
| [docker/login-action](https://github.com/docker/login-action) | `4.1.0` | `4.2.0` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | `7.1.0` | `7.2.0` |
| [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) | `9.2.0` | `9.2.1` |
| [actions/stale](https://github.com/actions/stale) | `10.2.0` | `10.3.0` |


Updates `actions/checkout` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10)

Updates `github/codeql-action` from 4.35.3 to 4.36.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/e46ed2cbd01164d986452f91f178727624ae40d7...87557b9c84dde89fdd9b10e88954ac2f4248e463)

Updates `docker/setup-qemu-action` from 4.0.0 to 4.1.0
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/ce360397dd3f832beb865e1373c09c0e9f86d70a...06116385d9baf250c9f4dcb4858b16962ea869c3)

Updates `docker/setup-buildx-action` from 4.0.0 to 4.1.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd...d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5)

Updates `docker/metadata-action` from 6.0.0 to 6.1.0
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/030e881283bb7a6894de51c315a6bfe6a94e05cf...80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9)

Updates `docker/login-action` from 4.1.0 to 4.2.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/4907a6ddec9925e35a0a9e82d7399ccc52663121...650006c6eb7dba73a995cc03b0b2d7f5ca915bee)

Updates `docker/build-push-action` from 7.1.0 to 7.2.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/bcafcacb16a39f128d818304e6c9c0c18556b85f...f9f3042f7e2789586610d6e8b85c8f03e5195baf)

Updates `golangci/golangci-lint-action` from 9.2.0 to 9.2.1
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/1e7e51e771db61008b38414a730f564565cf7c20...82606bf257cbaff209d206a39f5134f0cfbfd2ee)

Updates `actions/stale` from 10.2.0 to 10.3.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/b5d41d4e1d5dceea10e7104786b73624c18a190f...eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/metadata-action
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/login-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/build-push-action
  dependency-version: 7.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: golangci/golangci-lint-action
  dependency-version: 9.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/stale
  dependency-version: 10.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 19:52:05 -07:00
jvoisin 102989656b perf(template): precompute static icon URLs at parse time
iconPath() is called a bunch of times on virtually every pages. Each call did a
fmt.Sprintf to build a string of the form
"{basePath}/icon/{checksum}/{filename}". Since both the base path and
BinaryBundles are fixed at startup, all results are determinate.

This commit precomputes the full URL for every embedded bundle once in
funcMap.Map() into a map[string]string and turns iconPath into a single map
lookup. The "_/" fallback for unknown filenames was unused, as every caller
passes a compile-time literal present in bin/, so it was removed.

Microbenchmarks are showing stupidly high gains of course, but anything macro
is non-trivial, and I gave up on it. Knowing that it removes at least one heap
allocation and a couple of reflection calls on every iconPath call is enough to
bring me joy.
2026-06-02 20:58:34 -07:00
jvoisin 533ff80744 perf(ui): add max-age to Cache-Control
While inspecting the network requests done by miniflux' web interface, I
noticed that immutable assets with the `immutable` header were still fetched.
So I reread
https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control:

> immutable tells a cache that the response is immutable while it's fresh and
avoids those kinds of unnecessary conditional requests to the server.

So it needs to have max-age as well, otherwise the browser will consider the
resource as not fresh, and will perform a network request (and get a 304).
2026-06-02 20:52:20 -07:00
jvoisin 83ea3d1912 security(user): don't leak usernames existence via timing
When a non-existent username was submitted, CheckPassword returned
immediately without performing a bcrypt comparison, making it possible
to distinguish valid from invalid usernames by measuring response time.

Perform a dummy bcrypt comparison against a fixed cost-10 hash when
the user is not found so the response time is indistinguishable from
a real password check.
2026-06-02 20:44:42 -07:00
jvoisin 651fbd1560 perf(filter): cache compiled regexes across entries
Filter rules are evaluated once per entry on every feed refresh. The
previous code called regexp.Compile / regexp.MatchString per call,
recompiling the same patterns N times per refresh: once per entry per
filter rule, plus once per entry for feed.BlocklistRules and
feed.KeeplistRules.

This commit routes all regex compilations through a small cachedRegex() helper
that memoizes results in a process-wide map (RWMutex protected, since we need
len() and atomic reset that sync.Map doesn't expose). A nil cached value means
the pattern previously failed to compile.

To prevent unbounded memory growth from an authenticated user churning
distinct patterns, the cache is completely reset once it reaches
maxCachedRegexes entries.

Benchmarked on a 50-entry refresh with 6 distinct regex rules:

  before:  ~408 µs/op   11,580 B/op   131 allocs/op
  after:   ~271 µs/op       ~0 B/op     0 allocs/op

Making it roughly 33% faster with zero allocations per feed-refresh batch,
scaling linearly with entry count, yay.
2026-06-01 19:37:50 -07:00
jvoisin 5e766332be perf(storage): replace correlated subqueries in CategoriesWithFeedCount
The old query ran two correlated subqueries per category row: one to
count feeds and one to count unread entries. For N categories this meant
2xN subquery executions.

This commit replaces them with two pre-aggregated subqueries joined once to
categories.

The change was validated against my live database with 5 categories, ~500 feeds
and 12k unreads:

|                     | Old (correlated)    | New (pre-aggregated)   |
|---------------------|---------------------|------------------------|
| Execution time      | 3.216 ms            | 3.123 ms               |
| Planning time       | 0.732 ms            | 0.778 ms               |
| Buffer hits         | 3,141               | 540                    |
| Index searches      | 1,237               | 1                      |
| Seq scans on feeds  | 10 (2 per category) | 2 (once each subquery) |
| Subquery executions | 10 (2x5 categories) | 2 (fixed)              |
2026-06-01 17:25:33 -07:00
Dennis Eriksen 2717336d2c docs: Specify unit of time for POLLING_FREQUENCY and list-separation for TRUSTED_REVERSE_PROXY_NETWORKS 2026-05-30 19:18:56 -07:00
gudvinr 9a774083ab refactor(response): use http.Header for header map
http.Header is built for headers, so use it for its intended purpose
v2.3.1 2.3.1
2026-05-28 16:13:04 -07:00
dependabot[bot] 79381e6f9a build(deps): bump the gomod group with 4 updates
Bumps the gomod group with 4 updates: [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn), [golang.org/x/crypto](https://github.com/golang/crypto), [golang.org/x/image](https://github.com/golang/image) and [golang.org/x/net](https://github.com/golang/net).


Updates `github.com/go-webauthn/webauthn` from 0.17.3 to 0.17.4
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Changelog](https://github.com/go-webauthn/webauthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.17.3...v0.17.4)

Updates `golang.org/x/crypto` from 0.51.0 to 0.52.0
- [Commits](https://github.com/golang/crypto/compare/v0.51.0...v0.52.0)

Updates `golang.org/x/image` from 0.40.0 to 0.41.0
- [Commits](https://github.com/golang/image/compare/v0.40.0...v0.41.0)

Updates `golang.org/x/net` from 0.54.0 to 0.55.0
- [Commits](https://github.com/golang/net/compare/v0.54.0...v0.55.0)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-version: 0.17.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gomod
- dependency-name: golang.org/x/crypto
  dependency-version: 0.52.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
- dependency-name: golang.org/x/image
  dependency-version: 0.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-27 17:35:18 -07:00
Frédéric Guillot 161ed71eb0 fix(api): forbid setting OAuth identity fields via user update
A non-admin could bind an arbitrary OAuth identity to their own account
by patching google_id or openid_connect_id, bypassing the duplicate
check enforced in the OAuth callback. Remove both fields from the
update request so binding only happens through the OAuth flow.
2026-05-26 21:04:56 -07:00
Frédéric Guillot c896bafdaa fix(urllib): reject backslashes in relative path validation
Browsers normalize backslashes to forward slashes, so a redirect target
like "/\evil.com" parsed as a relative path by url.Parse and resolved to
//evil.com by the browser, resulting in an open redirect. Reject any link
containing a backslash before validating it as a relative path.
2026-05-26 20:06:58 -07:00
gudvinr 66996e3ffa refactor(storage): use same API for list and singular items
Instead of relying on WithX and WithXs just stick to latter with parametrized arguments.
2026-05-26 19:21:25 -07:00
jvoisin f050f23bda refactor(storage): collapse WithTags into a single array-containment predicate
WithTags previously emitted one "LOWER($i) = ANY(LOWER(e.tags::text)::text[])"
condition and one parameter per filter tag. With K tags this means K
predicates, K parameters, and K evaluations of the LOWER(e.tags::text)::text[]
sub-expression per candidate row.

This commit replaces the loop with a single predicate using the array
containment operator:

    LOWER(e.tags::text)::text[] @> LOWER($N::text)::text[]

Same case-insensitive "row must contain all listed tags" semantics, fewer
predicates for the planner, and the row-side expression is referenced once
instead of K times.
2026-05-26 19:08:38 -07:00
jvoisin 50088405e0 perf(reader): walk feed <link> tags in a single DOM pass
findSubscriptionsFromWebPage was running four separate goquery.Find passes, one
per supported MIME type. This commit replaces it with one
doc.Find("link[type]") traversal and a switch on the type attribute, which is
functionally equivalent, but it visits the DOM once instead of four times and
emits results in document order. Microbenchmark on a representative <head> (8
link tags + ~100 unrelated head children):

  before: ~25 µs/op, 1536 B/op, 50 allocs/op
  after:  ~9  µs/op,  744 B/op, 21 allocs/op
2026-05-25 20:01:25 -07:00
gudvinr d235a63138 refactor(sanitizer): always trim spaces in StripTags
It's never used without being trimmed anyway
2026-05-25 18:07:09 -07:00
jvoisin 483da488d8 perf(finder): size findSubscriptionsFromWebPage dedup map 2026-05-24 17:50:27 -07:00
gudvinr e01a6100ca fix(readingtime): make CJK detection more reliable
* division by 50 is 2%, not 50%
* non-letters are often the same between LTR languages
2026-05-24 17:40:08 -07:00
gudvinr a725476164 fix(readingtime): trim CJK text by rune not by bytes
Common mistake when working with UTF-8 is to use sub-slicing for truncate. That splits multi-byte runes in half breaking encoding.
2026-05-24 17:40:08 -07:00
jvoisin 47e304f343 perf(integration): don't defer in a for loop
Since the Body isn't used, it can immediately be closed, instead of deferring
the operation to the end of the function.
2026-05-23 21:35:47 -07:00