Skip to content

feat(app-form): paginate the icon picker and auto-detect app favicons - #6248

Open
junkerderprovinz wants to merge 3 commits into
homarr-labs:devfrom
junkerderprovinz:feat/icon-picker-pagination-and-favicon
Open

feat(app-form): paginate the icon picker and auto-detect app favicons#6248
junkerderprovinz wants to merge 3 commits into
homarr-labs:devfrom
junkerderprovinz:feat/icon-picker-pagination-and-favicon

Conversation

@junkerderprovinz

@junkerderprovinz junkerderprovinz commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Two related improvements to the app-creation icon flow, bundled since they converge on the same form and icon router.

Closes #4330
Closes #2480

#4330 — Icon picker: show more than 12 icons per repository

The picker requested only the router default of 12 icons per repository, so repositories with many matches (e.g. selfh.st) were silently truncated with no way to reach the rest. The backend already accepts a limitPerGroup (1–500), so this is a frontend change: the picker now tracks the limit in state, passes it to icon.findIcons, and renders a "Show more icons" button that raises it in steps (12 → 48 → 192 → 500) while any repository is still truncated. Each new search resets to the compact 12 again.

#2480 — Auto-detect the app icon from its URL

Requested by @Meierschlumpf. When creating or editing an app that has no icon chosen yet, Homarr now fetches the linked page and reads its <link rel="icon"> / <link rel="apple-touch-icon"> declarations (largest size wins), falling back to the origin's /favicon.ico. This mirrors the existing name-based auto-selection right next to it in _form.tsx and only sets the icon while the field is still empty, so it never overrides a user's choice.

Security note (favicon fetch): the fetch happens in an authenticated protectedProcedure, is restricted to http(s) schemes, follows redirects, and is bounded in both response size (512 KB, only the <head> is needed) and time (5 s). Private/LAN targets are intentionally not blocked — Homarr apps overwhelmingly live on the local network, and this is the same trust model as configuring an integration or ping URL that the server already fetches. Happy to tighten this further if you'd prefer.

New helper packages/icons/src/favicon-fetcher.ts with unit tests covering the preference order (apple-touch-icon > largest declared size), the /favicon.ico fallback, the no-icon case, and non-http scheme rejection.

Checklist

  • Builds without errors (tsc --noEmit clean on all touched packages: icons, api, validation, forms-collection, translation)
  • Formatted with oxfmt, oxlint clean (only the pre-existing exhaustive-deps warning that the neighbouring name-based effect already emits)
  • Targets dev
  • Conventional commits (one per feature)
  • No shorthand variable names
  • Docs: a follow-up PR to homarr-labs/documentation will note the auto-favicon behaviour on the apps page — happy to open it once the approach here is confirmed.

One new i18n key (common.iconPicker.showMore) added to the en.json source only, per the Crowdin flow.

Summary by CodeRabbit

  • New Features

    • Automatically detects and suggests an app icon from a site’s favicon when you paste a URL.
    • The icon picker now supports “Show more icons” pagination to load additional results.
  • Bug Fixes

    • Improved icon discovery to better select the most suitable icon when multiple candidates are available, with improved fallback behavior.
  • Tests

    • Added/expanded automated coverage for favicon/icon URL discovery scenarios.

The icon picker requested only the default of 12 icons per repository, so
repositories with many matching icons (for example selfh.st) were silently
truncated with no way to reach the rest. Track the per-repository limit in
state, pass it to the icon search, and add a "Show more icons" button that
raises it in steps up to the router maximum. Each new search starts compact
again.

Closes homarr-labs#4330
When creating or editing an app without a chosen icon yet, fetch the linked
page and read its `<link rel="icon">` / `apple-touch-icon` declarations,
falling back to the origin's `/favicon.ico`. This mirrors the existing
name-based icon auto-selection.

The fetch runs in an authenticated tRPC procedure, is restricted to http(s)
schemes and bounded in both response size and time. Private/LAN targets are
intentionally allowed, since Homarr apps commonly live on the local network
and that is the same trust model as configuring an integration URL.

Closes homarr-labs#2480
@junkerderprovinz
junkerderprovinz requested a review from a team as a code owner July 7, 2026 20:28
@dokploy-homarr-labs

Copy link
Copy Markdown

🚨 Preview Deployment Blocked - Security Protection

Your pull request was blocked from triggering preview deployments

Why was this blocked?

  • User: junkerderprovinz
  • Repository: homarr
  • Permission Level: read
  • Required Level: write, maintain, or admin

How to resolve this:

Option 1: Get Collaborator Access (Recommended)
Ask a repository maintainer to invite you as a collaborator with write permissions or higher.

Option 2: Request Permission Override
Ask a repository administrator to disable security validation for this specific application if appropriate.

For Repository Administrators:

To disable this security check (⚠️ not recommended for public repositories):
Enter to preview settings and disable the security check.


This security measure protects against malicious code execution in preview deployments. Only trusted collaborators should have the ability to trigger deployments.

🛡️ Learn more about this security feature

This protection prevents unauthorized users from:

  • Executing malicious code on the deployment server
  • Accessing environment variables and secrets
  • Potentially compromising the infrastructure

Preview deployments are powerful but require trust. Only users with repository write access can trigger them.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e99526c-ae24-4654-86e0-c43af19141b3

📥 Commits

Reviewing files that changed from the base of the PR and between 68f35df and b084ecd.

📒 Files selected for processing (2)
  • packages/api/src/router/icons.ts
  • packages/icons/src/favicon-fetcher.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/api/src/router/icons.ts
  • packages/icons/src/favicon-fetcher.ts

📝 Walkthrough

Walkthrough

This PR adds automatic favicon detection through a bounded HTML fetcher, protected API endpoint, and app-form integration. It also adds stepped icon-result pagination with a “show more” control.

Changes

Automatic favicon detection

Layer / File(s) Summary
Favicon fetching core logic
packages/icons/src/favicon-fetcher.ts, packages/icons/src/favicon-fetcher.spec.ts, packages/icons/index.ts
Fetches and parses linked icons, selects candidates by relation and size, falls back to /favicon.ico, and exports the utility with Vitest coverage.
Validation schema and API endpoint
packages/validation/src/icons.ts, packages/api/src/router/icons.ts
Validates HTTP(S) hrefs and adds the protected getIconForUrl query returning the resolved icon URL.
App form auto-icon wiring
packages/forms-collection/src/new-app/_form.tsx
Debounces the app URL, queries favicon detection when no icon is selected, and assigns the returned URL to iconUrl.

Icon picker pagination

Layer / File(s) Summary
Stepped limit state and query wiring
packages/forms-collection/src/icon-picker/icon-picker.tsx
Adds stepped per-group limits, resets the limit on search changes, passes it to the icon query, and detects additional results.
Show more button rendering
packages/forms-collection/src/icon-picker/icon-picker.tsx, packages/translation/src/lang/en.json
Adds a conditional show-more button that advances the limit using a new translation label.

Sequence Diagram(s)

sequenceDiagram
  participant AppForm
  participant iconsRouter
  participant fetchBestIconUrlForAppAsync
  participant TargetWebsite

  AppForm->>iconsRouter: getIconForUrl({ href })
  iconsRouter->>fetchBestIconUrlForAppAsync: resolve app icon
  fetchBestIconUrlForAppAsync->>TargetWebsite: fetch HTML
  TargetWebsite-->>fetchBestIconUrlForAppAsync: HTML response
  fetchBestIconUrlForAppAsync->>fetchBestIconUrlForAppAsync: select linked icon
  fetchBestIconUrlForAppAsync->>TargetWebsite: check /favicon.ico when needed
  TargetWebsite-->>fetchBestIconUrlForAppAsync: reachability response
  fetchBestIconUrlForAppAsync-->>iconsRouter: icon URL or null
  iconsRouter-->>AppForm: { url }
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the two main changes: icon picker pagination and automatic favicon detection.
Linked Issues check ✅ Passed The PR satisfies #4330 by adding a show-more flow for icon groups and #2480 by auto-fetching icons from declared links or /favicon.ico.
Out of Scope Changes check ✅ Passed The extra schema, translation, exports, and tests directly support the requested icon picker and favicon-fetch features.
Yagni / Over-Engineering ✅ Passed The change stays focused on the requested icon features; helpers are small, no new dependency/abstraction churn, and the favicon fetcher has a runnable spec.
Docs Are Up To Date ✅ Passed Not applicable: this PR only changes app icon flow; it adds no widget/integration and makes no apps/docs changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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: 2

🧹 Nitpick comments (3)
packages/forms-collection/src/icon-picker/icon-picker.tsx (1)

63-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Derive the max limit from limitPerGroupSteps instead of hardcoding 500.

limitPerGroupSteps already defines the max step (500), but hasMoreIcons (Line 88) hardcodes 500 separately and showMoreIcons (Line 91) also falls back to a hardcoded 500. If the steps array is ever changed, these values could silently drift out of sync.

♻️ Proposed refactor
+const maxLimitPerGroup = limitPerGroupSteps[limitPerGroupSteps.length - 1]!;
+
 const hasMoreIcons =
-  limitPerGroup < 500 && (data?.icons.some((group) => group.icons.length >= limitPerGroup) ?? false);
+  limitPerGroup < maxLimitPerGroup && (data?.icons.some((group) => group.icons.length >= limitPerGroup) ?? false);

 const showMoreIcons = () => {
-  setLimitPerGroup((current) => limitPerGroupSteps.find((step) => step > current) ?? 500);
+  setLimitPerGroup((current) => limitPerGroupSteps.find((step) => step > current) ?? maxLimitPerGroup);
 };

Separately, note that hasMoreIcons uses group.icons.length >= limitPerGroup as a proxy for "truncated" since the API only returns an overall countIcons, not a per-group total. A group whose true total exactly equals the current limit will show a "show more" button that yields no new icons on click (it self-corrects on the next render since the returned length then becomes smaller than the new limit), which is a minor, self-healing cosmetic quirk rather than a functional bug.

Also applies to: 75-93

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/forms-collection/src/icon-picker/icon-picker.tsx` at line 63, The
icon picker is hardcoding the max limit in `hasMoreIcons` and `showMoreIcons`
instead of deriving it from `limitPerGroupSteps`, which can drift if the steps
change. Update the `IconPicker` logic to compute the maximum from the last entry
in `limitPerGroupSteps` and use that shared value wherever the fallback or
“more” threshold is needed. Keep the existing `useState(initialLimitPerGroup)`
and related pagination behavior intact, but replace the literal max values in
the relevant `IconPicker` helpers with the derived constant.
packages/validation/src/icons.ts (1)

8-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

.url() chain method is deprecated in Zod v4.

Zod v4 promotes string-format validators to top-level functions; z.string().url() still works but is deprecated in favor of z.url().

♻️ Suggested update
 export const iconForUrlSchema = z.object({
   // Only http(s) targets are fetched server-side; other schemes are rejected.
-  href: z
-    .string()
-    .trim()
-    .url()
-    .regex(/^https?:\/\//i),
+  href: z
+    .url()
+    .regex(/^https?:\/\//i),
 });

Note: confirm this matches the exact Zod version pinned in the repo before applying, and that .trim() composition still fits the top-level z.url() API (may require reordering as a pipe/transform).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/validation/src/icons.ts` around lines 8 - 15, The href validator in
iconForUrlSchema still uses the deprecated z.string().url() chain. Update this
schema to use the Zod v4 top-level URL validator in a way that preserves the
existing trim and https-only regex behavior, likely by reordering the checks or
piping/transforms around z.url(). Make sure the final shape in icons.ts still
rejects non-http(s) schemes and matches the repo’s pinned Zod version before
changing the API usage.
packages/icons/src/favicon-fetcher.ts (1)

115-186: 🚀 Performance & Scalability | 🔵 Trivial

Regex-based <link> parsing is fragile for malformed/edge-case HTML.

linkTagRegex/attributeRegex work for the tested cases but won't handle things like link tags inside HTML comments, unquoted attributes with > in values, or non-standard whitespace/ordering as robustly as a real parser. Given the bounded, simple use case this is likely fine for now, but consider a lightweight HTML parser if false negatives on real-world sites become an issue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/icons/src/favicon-fetcher.ts` around lines 115 - 186, The `<link>`
extraction in `extractBestIconUrl` relies on `linkTagRegex` and
`attributeRegex`, which is too brittle for malformed or edge-case HTML. Replace
the regex-based parsing with a lightweight HTML parser or DOM-like parser that
can accurately walk `<link>` elements and read `rel`, `href`, and `sizes`
attributes. Keep `parseAttributes`, `parseSizeArea`, and `resolveAbsoluteUrl`
behavior conceptually the same, but feed them data from the parser instead of
raw tag strings. Ensure `extractBestIconUrl` still filters by icon-related rel
values and selects the best candidate using relation score and size.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/api/src/router/icons.ts`:
- Around line 9-11: The new getIconForUrl tRPC query is missing MCP exposure.
Update the procedure chain in iconsRouter so getIconForUrl includes a .meta({
mcp: { enabled: true, description: "..." } }) call before .input(), matching the
pattern used by findIcons and making the new icon-resolution query available as
an MCP tool.

In `@packages/icons/src/favicon-fetcher.ts`:
- Around line 141-148: The sorting logic in favicon-fetcher currently
hard-prioritizes relationScore in the candidates comparator, so a low-resolution
apple-touch-icon can outrank a much larger icon. Update the comparator in the
candidate selection logic to make sizeArea the primary signal and use
relationScore only as a tiebreaker when sizes are comparable, preserving the
intent of picking the best-quality icon. Keep the fix localized to the
candidates.sort block and the return candidates[0] selection.

---

Nitpick comments:
In `@packages/forms-collection/src/icon-picker/icon-picker.tsx`:
- Line 63: The icon picker is hardcoding the max limit in `hasMoreIcons` and
`showMoreIcons` instead of deriving it from `limitPerGroupSteps`, which can
drift if the steps change. Update the `IconPicker` logic to compute the maximum
from the last entry in `limitPerGroupSteps` and use that shared value wherever
the fallback or “more” threshold is needed. Keep the existing
`useState(initialLimitPerGroup)` and related pagination behavior intact, but
replace the literal max values in the relevant `IconPicker` helpers with the
derived constant.

In `@packages/icons/src/favicon-fetcher.ts`:
- Around line 115-186: The `<link>` extraction in `extractBestIconUrl` relies on
`linkTagRegex` and `attributeRegex`, which is too brittle for malformed or
edge-case HTML. Replace the regex-based parsing with a lightweight HTML parser
or DOM-like parser that can accurately walk `<link>` elements and read `rel`,
`href`, and `sizes` attributes. Keep `parseAttributes`, `parseSizeArea`, and
`resolveAbsoluteUrl` behavior conceptually the same, but feed them data from the
parser instead of raw tag strings. Ensure `extractBestIconUrl` still filters by
icon-related rel values and selects the best candidate using relation score and
size.

In `@packages/validation/src/icons.ts`:
- Around line 8-15: The href validator in iconForUrlSchema still uses the
deprecated z.string().url() chain. Update this schema to use the Zod v4
top-level URL validator in a way that preserves the existing trim and https-only
regex behavior, likely by reordering the checks or piping/transforms around
z.url(). Make sure the final shape in icons.ts still rejects non-http(s) schemes
and matches the repo’s pinned Zod version before changing the API usage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bb128ce6-28b4-45a7-8ef7-8dbb2ba4f147

📥 Commits

Reviewing files that changed from the base of the PR and between 260b167 and 68f35df.

📒 Files selected for processing (8)
  • packages/api/src/router/icons.ts
  • packages/forms-collection/src/icon-picker/icon-picker.tsx
  • packages/forms-collection/src/new-app/_form.tsx
  • packages/icons/index.ts
  • packages/icons/src/favicon-fetcher.spec.ts
  • packages/icons/src/favicon-fetcher.ts
  • packages/translation/src/lang/en.json
  • packages/validation/src/icons.ts

Comment thread packages/api/src/router/icons.ts Outdated
Comment thread packages/icons/src/favicon-fetcher.ts
Comment thread packages/api/src/router/icons.ts Outdated
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";

export const iconsRouter = createTRPCRouter({
getIconForUrl: protectedProcedure.input(iconForUrlSchema).query(async ({ input }) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why protected ? Wouldn't that need a normal public procedure in order to use it for other places as well ? Or is it used only within the add creation / edit process ?

@junkerderprovinz

Copy link
Copy Markdown
Contributor Author

@ajnart Good question! Right now it's only used inside the add/edit flow, so protected already covers it.

I kept it protected on purpose: it does a server-side fetch of a user-supplied URL to auto-detect the favicon, and exposing that as a public (unauthenticated) procedure would turn it into an SSRF vector, since anyone could make the server fetch arbitrary URLs. Any other place that ends up needing it would be behind auth too, so protected still works there.

That said, I'm totally happy to switch it to public if you'd rather have it available more broadly. My vote would be to keep it protected for that reason, but just let me know what you prefer and I'll adjust. Thanks for taking a look!

@junkerderprovinz

Copy link
Copy Markdown
Contributor Author

Hi @ajnart — following up on your question about the favicon-detect procedure: I kept it as a protectedProcedure on purpose, because a public server-side fetch of a user-supplied URL would open an SSRF vector. Happy to adjust if you'd prefer a different approach; otherwise it's green and ready. Thanks!

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.

feat(icon-picker): show more than 12 icons per source feat: fetch app icons automatically

2 participants