Repository navigation
feat(favicons): one server route for every site icon, with parent-domain fallback #9494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
6c5ba2e
feat(favicons): resolve every site icon through one server route with…
waleedlatif1 a87bfde
fix(favicons): 204 for no icon, only 404 is a provider miss, reject e…
waleedlatif1 9109940
fix(favicons): keep the presenter's 204 literal and read only a 200 b…
waleedlatif1 ddc6806
fix(favicons): annotate the presenter so each branch is checked again…
waleedlatif1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { getFaviconContract } from '@/lib/api/contracts/favicon' | ||
| import { | ||
| defineInternalBinaryRoute, | ||
| internalOrchestrationErrorPolicy, | ||
| internalRateLimits, | ||
| internalSessionAuth, | ||
| } from '@/lib/api/server/routes' | ||
| import type { | ||
| BinaryResponseDescriptor, | ||
| NoContentResponseDescriptor, | ||
| } from '@/lib/api/server/routes/types' | ||
| import { getFavicon } from '@/lib/favicon/application' | ||
|
|
||
| export const GET = defineInternalBinaryRoute({ | ||
| contract: getFaviconContract, | ||
| auth: internalSessionAuth, | ||
| operation: getFavicon.operation, | ||
| rateLimit: internalRateLimits.user({ | ||
| bucketName: 'favicon', | ||
| /** A page can render dozens of icons at once; the browser caches each one after its first load. */ | ||
| config: { maxTokens: 300, refillRate: 300, refillIntervalMs: 60_000 }, | ||
| }), | ||
| errorPolicy: internalOrchestrationErrorPolicy, | ||
| mapInput: ({ query }) => ({ host: query.host }), | ||
| useCase: getFavicon, | ||
| /** No icon is an ordinary answer, not an error: 204, which an `<img>` still reports as failed. */ | ||
| present: (favicon): BinaryResponseDescriptor | NoContentResponseDescriptor => | ||
| favicon.kind === 'hit' | ||
| ? { | ||
| body: new Uint8Array(favicon.bytes), | ||
| contentType: favicon.contentType, | ||
| contentLength: favicon.bytes.length, | ||
| headers: { | ||
| 'Cache-Control': 'private, max-age=86400, stale-while-revalidate=604800', | ||
| 'X-Content-Type-Options': 'nosniff', | ||
| }, | ||
| } | ||
| : { | ||
| status: 204, | ||
| headers: { | ||
| 'Cache-Control': favicon.kind === 'miss' ? 'private, max-age=3600' : 'no-store', | ||
| }, | ||
| }, | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,30 +1,14 @@ | ||
| 'use client' | ||
|
|
||
| import { useState } from 'react' | ||
| import { Globe } from '@sim/emcn/icons' | ||
| import { faviconUrl } from '@/lib/core/utils/favicon' | ||
| import { Favicon } from '@/components/favicon' | ||
| import { simLinkOrigin } from '@/lib/sim-links/validation' | ||
|
|
||
| interface SimLinkIconProps { | ||
| /** The link's destination; a variable link's `{*}` never changes its host. */ | ||
| url: string | ||
| } | ||
|
|
||
| /** The destination site's favicon, or a globe when the site has none. */ | ||
| /** The destination site's favicon, or a globe when neither it nor a parent domain has one. */ | ||
| export function SimLinkIcon({ url }: SimLinkIconProps) { | ||
| const [failedHostname, setFailedHostname] = useState<string | null>(null) | ||
| const origin = simLinkOrigin(url) | ||
| const hostname = origin ? new URL(origin).hostname : null | ||
| if (!hostname || failedHostname === hostname) return <Globe /> | ||
| return ( | ||
| <img | ||
| src={faviconUrl(hostname, 64)} | ||
| alt='' | ||
| referrerPolicy='no-referrer' | ||
| loading='lazy' | ||
| decoding='async' | ||
| className='rounded-sm' | ||
| onError={() => setFailedHostname(hostname)} | ||
| /> | ||
| ) | ||
| return <Favicon host={origin ? new URL(origin).hostname : null} fallback={<Globe />} /> | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| 'use client' | ||
|
|
||
| import { type ReactNode, useState } from 'react' | ||
| import { cn } from '@sim/emcn' | ||
| import { faviconSrc } from '@/lib/favicon/url' | ||
|
|
||
| interface FaviconProps { | ||
| /** The site's hostname; without one the fallback renders. */ | ||
| host: string | null | undefined | ||
| /** Rendered when there is no host, or when neither the host nor a parent domain has an icon. */ | ||
| fallback: ReactNode | ||
| /** Sizing for the icon; the fallback carries its own. */ | ||
| className?: string | ||
| } | ||
|
|
||
| /** | ||
| * A site's icon, served by Sim's favicon route: the browser never contacts a third party, a | ||
| * private tenant shows its platform's icon, and a site with none renders `fallback` instead | ||
| * of a provider's placeholder. | ||
| */ | ||
| export function Favicon({ host, fallback, className }: FaviconProps) { | ||
| const [failedHost, setFailedHost] = useState<string | null>(null) | ||
| if (!host || failedHost === host) return fallback | ||
| return ( | ||
| <img | ||
| src={faviconSrc(host)} | ||
| alt='' | ||
| loading='lazy' | ||
| decoding='async' | ||
| className={cn('rounded-sm', className)} | ||
| onError={() => setFailedHost(host)} | ||
| /> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { z } from 'zod' | ||
| import { defineRouteContract } from '@/lib/api/contracts/types' | ||
|
|
||
| /** | ||
| * A site's icon by hostname: image bytes, or 204 when none can be served — neither the host nor | ||
| * a parent has one, the host is internal, or the providers are unavailable. | ||
| */ | ||
| export const getFaviconContract = defineRouteContract({ | ||
| method: 'GET', | ||
| path: '/api/favicon', | ||
| query: z.object({ | ||
| host: z | ||
| .string() | ||
| .trim() | ||
| .min(1, 'host is required') | ||
| .max(253, 'host must be 253 characters or less'), | ||
| }), | ||
| response: { mode: 'binary', status: [200, 204] }, | ||
| }) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.