feat(apps): distinguish local development with blue blueprint favicons (#51420)
## Problem - Local apps are hard to distinguish from hosted apps in browser tabs. ## Solution - Add blue blueprint icons to local development in all 8 apps and Studio CLI. - Keep normal branding for production, hosted staging and self-hosted Studio. - Builds on #51419. | Before | After | | --- | --- | | <img width="1290" height="76" alt="CleanShot 2026-10-08 at 13 29 14@2x" src="https://github.com/user-attachments/assets/72bc50d7-37e0-47d0-8dfd-50e233a443bd" /> | <img width="1282" height="72" alt="CleanShot 2026-10-08 at 13 30 03@2x" src="https://github.com/user-attachments/assets/afb15a70-3938-4cff-9b41-dc6c25460b7c" /> | ## Review instructions - Start an app locally. Its browser tab should show the blue icon. - Check Studio CLI uses blue and hosted staging uses white. - Check production icons are unchanged, including Learn's black and white icon. - Run `node scripts/validate-favicons.mjs`. All 16 sets should pass. Asset and routing checks, formatting and Common/Studio type checks passed. Full builds have not been run. ## Checklist - [x] I have read [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
No files matched your search
@@ -11,12 +11,14 @@ import { inter, manrope, sourceCodePro } from '@/lib/fonts'
|
||||
|
||||
const className = `${inter.variable} ${manrope.variable} ${sourceCodePro.variable}`
|
||||
|
||||
const FAVICON_ROUTE = process.env.NODE_ENV === 'development' ? '/favicon/local' : '/favicon'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
applicationName: 'Supabase Design System',
|
||||
title: 'Supabase Design System',
|
||||
description: 'Design resources for building consistent user experiences at Supabase.',
|
||||
icons: genFaviconData(BASE_PATH),
|
||||
manifest: `${BASE_PATH}/favicon/manifest.json`,
|
||||
icons: genFaviconData(BASE_PATH, FAVICON_ROUTE),
|
||||
manifest: `${BASE_PATH}${FAVICON_ROUTE}/manifest.json`,
|
||||
}
|
||||
|
||||
export const viewport: Viewport = {
|
||||
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Supabase Design System",
|
||||
"short_name": "Supabase Design System",
|
||||
"description": "Design resources for building consistent user experiences at Supabase.",
|
||||
"display": "standalone",
|
||||
"theme_color": "#1C1C1C",
|
||||
"background_color": "#1C1C1C",
|
||||
"icons": [
|
||||
{
|
||||
"src": "android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "android-icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"env": ["NEXT_PUBLIC_BASE_PATH", "NEXT_PUBLIC_APP_URL"],
|
||||
"env": ["NODE_ENV", "NEXT_PUBLIC_BASE_PATH", "NEXT_PUBLIC_APP_URL"],
|
||||
"outputs": [".next/**", "!.next/cache/**", "!.next/dev/**/*", ".velite/**"],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -19,14 +19,16 @@ const { metadataApplicationName, metadataTitle } = getCustomContent([
|
||||
'metadata:title',
|
||||
])
|
||||
|
||||
const FAVICON_ROUTE = process.env.NODE_ENV === 'development' ? '/favicon/local' : '/favicon'
|
||||
|
||||
const metadata: Metadata = {
|
||||
applicationName: metadataApplicationName,
|
||||
title: metadataTitle,
|
||||
description:
|
||||
'Supabase is the Postgres development platform providing all the backend features you need to build a product.',
|
||||
metadataBase: new URL('https://supabase.com'),
|
||||
icons: genFaviconData(BASE_PATH),
|
||||
manifest: `${BASE_PATH}/favicon/manifest.json`,
|
||||
icons: genFaviconData(BASE_PATH, FAVICON_ROUTE),
|
||||
manifest: `${BASE_PATH}${FAVICON_ROUTE}/manifest.json`,
|
||||
robots: {
|
||||
index: IS_PRODUCTION,
|
||||
follow: IS_PRODUCTION,
|
||||
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Supabase",
|
||||
"short_name": "Supabase",
|
||||
"description": "The Postgres Development Platform.",
|
||||
"display": "standalone",
|
||||
"theme_color": "#1C1C1C",
|
||||
"background_color": "#1C1C1C",
|
||||
"icons": [
|
||||
{
|
||||
"src": "android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "android-icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Supabase Knowledge Base",
|
||||
"short_name": "Supabase",
|
||||
"icons": [
|
||||
{
|
||||
"src": "android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "android-icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,7 +10,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const { title, description } = Astro.props
|
||||
const faviconRoute = '/favicon'
|
||||
const faviconRoute = import.meta.env.DEV ? '/favicon/local' : '/favicon'
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
|
||||
@@ -14,13 +14,15 @@ const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? ''
|
||||
|
||||
const FAVICON_ROUTE = process.env.NODE_ENV === 'development' ? '/favicon/local' : '/favicon'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
applicationName: 'Learn Supabase',
|
||||
title: 'Learn Supabase',
|
||||
description: 'Learn Supabase.',
|
||||
metadataBase: new URL('https://supabase.com/learn'),
|
||||
icons: genFaviconData(BASE_PATH),
|
||||
manifest: `${BASE_PATH}/favicon/manifest.json`,
|
||||
icons: genFaviconData(BASE_PATH, FAVICON_ROUTE),
|
||||
manifest: `${BASE_PATH}${FAVICON_ROUTE}/manifest.json`,
|
||||
openGraph: {
|
||||
type: 'article',
|
||||
authors: 'Supabase',
|
||||
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Supabase",
|
||||
"short_name": "Supabase",
|
||||
"description": "The Postgres Development Platform.",
|
||||
"display": "standalone",
|
||||
"theme_color": "#1C1C1C",
|
||||
"background_color": "#1C1C1C",
|
||||
"icons": [
|
||||
{
|
||||
"src": "android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "android-icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"env": ["NODE_ENV"],
|
||||
"outputs": ["dist/**", ".next/**", "!.next/cache/**/*", "!.next/dev/**/*", ".velite/**"],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,6 +5,22 @@ import type { Route } from './+types/root'
|
||||
import '../styles/globals.css'
|
||||
|
||||
export const links: Route.LinksFunction = () => [
|
||||
...(import.meta.env.DEV
|
||||
? [
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/x-icon',
|
||||
sizes: '16x16 32x32 48x48',
|
||||
href: '/favicon/local/favicon.ico',
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
sizes: '180x180',
|
||||
href: '/favicon/local/apple-icon-180x180.png',
|
||||
},
|
||||
{ rel: 'manifest', href: '/favicon/local/manifest.json' },
|
||||
]
|
||||
: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]),
|
||||
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
||||
{
|
||||
rel: 'preconnect',
|
||||
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Supabase Lite Studio",
|
||||
"short_name": "Supabase",
|
||||
"icons": [
|
||||
{
|
||||
"src": "android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "android-icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getCLIFaviconRoute } from './cli-favicon'
|
||||
|
||||
vi.mock('@tanstack/react-start', () => ({
|
||||
createServerFn: () => ({ handler: (handler: () => string) => handler }),
|
||||
}))
|
||||
|
||||
afterEach(() => vi.unstubAllEnvs())
|
||||
|
||||
describe('CLI favicon route', () => {
|
||||
it('reads the CLI version at request time, after the module has loaded', async () => {
|
||||
vi.stubEnv('CURRENT_CLI_VERSION', '')
|
||||
expect(await getCLIFaviconRoute()).toBe('/favicon')
|
||||
|
||||
vi.stubEnv('CURRENT_CLI_VERSION', '2.50.0')
|
||||
expect(await getCLIFaviconRoute()).toBe('/favicon/local')
|
||||
|
||||
vi.stubEnv('CURRENT_CLI_VERSION', '')
|
||||
expect(await getCLIFaviconRoute()).toBe('/favicon')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createServerFn } from '@tanstack/react-start'
|
||||
|
||||
// Read at request time because the CLI sets this after the image is built.
|
||||
export const getCLIFaviconRoute = createServerFn({ method: 'GET' }).handler(() =>
|
||||
process.env.CURRENT_CLI_VERSION ? '/favicon/local' : '/favicon'
|
||||
)
|
||||
@@ -177,6 +177,13 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
|
||||
// [Joshen] Should target hosted staging, local dev, and local CLI only
|
||||
const isNonProdEnv = (IS_PLATFORM && process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod') || isCLI
|
||||
const isLocalEnv =
|
||||
process.env.NODE_ENV === 'development' ||
|
||||
process.env.NEXT_PUBLIC_ENVIRONMENT === 'local' ||
|
||||
isCLI
|
||||
let faviconRoute = '/favicon'
|
||||
if (isNonProdEnv) faviconRoute = '/favicon/staging'
|
||||
if (isLocalEnv) faviconRoute = '/favicon/local'
|
||||
|
||||
const checkCliEnvironment = async () => {
|
||||
const data = await getCLIReleaseVersion()
|
||||
@@ -220,7 +227,7 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
<MetaFaviconsPagesRouter
|
||||
includeManifest
|
||||
applicationName="Supabase Studio"
|
||||
route={isNonProdEnv ? '/favicon/staging' : '/favicon'}
|
||||
route={faviconRoute}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<RouteValidationWrapper>
|
||||
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "Supabase Studio",
|
||||
"short_name": "Supabase",
|
||||
"description": "The Postgres Development Platform.",
|
||||
"display_override": [
|
||||
"minimal-ui"
|
||||
],
|
||||
"display": "standalone",
|
||||
"theme_color": "#1C1C1C",
|
||||
"background_color": "#1C1C1C",
|
||||
"scope": "/",
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "android-icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -79,6 +79,7 @@ import { useCustomContent } from '@/hooks/custom-content/useCustomContent'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedOrganizationCreatedAtQuery } from '@/hooks/misc/useSelectedOrganizationCreatedAt'
|
||||
import { AuthProvider } from '@/lib/auth'
|
||||
import { getCLIFaviconRoute } from '@/lib/cli-favicon'
|
||||
import { toUnixSecondsString } from '@/lib/configcat-attributes'
|
||||
import { configureMonacoLoader } from '@/lib/configure-monaco-loader'
|
||||
import { API_URL, BASE_PATH, IS_PLATFORM, useDefaultProvider } from '@/lib/constants'
|
||||
@@ -204,11 +205,17 @@ const devToolbarExtraTabs: ExtraTab[] = IS_DEV_TOOLBAR_ENABLED
|
||||
|
||||
configureMonacoLoader()
|
||||
|
||||
const FAVICON_ROUTE = IS_NON_PROD_ENV ? '/favicon/staging' : '/favicon'
|
||||
// Build-time branding; CLI mode is resolved at runtime by the root loader.
|
||||
let FAVICON_ROUTE = '/favicon'
|
||||
if (IS_PLATFORM && process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod')
|
||||
FAVICON_ROUTE = '/favicon/staging'
|
||||
if (process.env.NODE_ENV === 'development' || process.env.NEXT_PUBLIC_ENVIRONMENT === 'local') {
|
||||
FAVICON_ROUTE = '/favicon/local'
|
||||
}
|
||||
const THEME_COLOR = '1E1E1E'
|
||||
const APPLICATION_NAME = 'Supabase Studio'
|
||||
|
||||
function buildRootHead() {
|
||||
function buildRootHead(faviconRoute = FAVICON_ROUTE) {
|
||||
const meta: Array<Record<string, string>> = [
|
||||
{ charSet: 'utf-8' },
|
||||
{ name: 'viewport', content: 'initial-scale=1.0, width=device-width' },
|
||||
@@ -220,8 +227,8 @@ function buildRootHead() {
|
||||
]
|
||||
|
||||
const links: Array<Record<string, string>> = [
|
||||
...genFaviconLinks(BASE_PATH, FAVICON_ROUTE),
|
||||
{ rel: 'manifest', href: `${BASE_PATH}${FAVICON_ROUTE}/manifest.json` },
|
||||
...genFaviconLinks(BASE_PATH, faviconRoute),
|
||||
{ rel: 'manifest', href: `${BASE_PATH}${faviconRoute}/manifest.json` },
|
||||
]
|
||||
|
||||
if (IS_PLATFORM) {
|
||||
@@ -279,7 +286,6 @@ function ErrorBoundaryRoute({ error }: ErrorComponentProps) {
|
||||
}
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
head: buildRootHead,
|
||||
// Mirrors the redirect rules in `next.config.ts` / `vercel.ts`. Vercel's
|
||||
// edge layer already handles these for the platform deploy; this is the
|
||||
// self-hosted (Node-server) fallback and the client-side safety net.
|
||||
@@ -308,6 +314,15 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
statusCode: match.permanent ? 308 : 307,
|
||||
})
|
||||
},
|
||||
loader: async (): Promise<string> => {
|
||||
if (IS_PLATFORM || FAVICON_ROUTE !== '/favicon') return FAVICON_ROUTE
|
||||
try {
|
||||
return await getCLIFaviconRoute()
|
||||
} catch {
|
||||
return FAVICON_ROUTE
|
||||
}
|
||||
},
|
||||
head: ({ loaderData }) => buildRootHead(loaderData),
|
||||
component: RootComponent,
|
||||
shellComponent: RootDocument,
|
||||
notFoundComponent: NotFound,
|
||||
|
||||
@@ -20,13 +20,15 @@ const sourceCodePro = Source_Code_Pro({
|
||||
|
||||
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? ''
|
||||
|
||||
const FAVICON_ROUTE = process.env.NODE_ENV === 'development' ? '/favicon/local' : '/favicon'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
applicationName: 'Supabase Library',
|
||||
title: 'Supabase Library',
|
||||
description: 'Supabase blocks and starter apps for authentication, storage, realtime, and more',
|
||||
metadataBase: new URL('https://supabase.com'),
|
||||
icons: genFaviconData(BASE_PATH),
|
||||
manifest: `${BASE_PATH}/favicon/manifest.json`,
|
||||
icons: genFaviconData(BASE_PATH, FAVICON_ROUTE),
|
||||
manifest: `${BASE_PATH}${FAVICON_ROUTE}/manifest.json`,
|
||||
openGraph: {
|
||||
type: 'article',
|
||||
authors: 'Supabase',
|
||||
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Supabase",
|
||||
"short_name": "Supabase",
|
||||
"description": "The Postgres Development Platform.",
|
||||
"display": "standalone",
|
||||
"theme_color": "#1C1C1C",
|
||||
"background_color": "#1C1C1C",
|
||||
"icons": [
|
||||
{
|
||||
"src": "android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "android-icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import { APP_NAME, DEFAULT_META_DESCRIPTION } from '@/lib/constants'
|
||||
|
||||
const site_title = `${APP_NAME} | The Postgres Development Platform`
|
||||
|
||||
const FAVICON_ROUTE = process.env.NODE_ENV === 'development' ? '/favicon/local' : '/favicon'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: site_title,
|
||||
description: DEFAULT_META_DESCRIPTION,
|
||||
@@ -34,8 +36,8 @@ export const metadata: Metadata = {
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
// www serves from the site root (`basePath: ''` in next.config.mjs)
|
||||
icons: genFaviconData(''),
|
||||
manifest: '/favicon/manifest.json',
|
||||
icons: genFaviconData('', FAVICON_ROUTE),
|
||||
manifest: `${FAVICON_ROUTE}/manifest.json`,
|
||||
}
|
||||
|
||||
export const viewport: Viewport = {
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
const forceDarkMode = isDarkLaunchWeek
|
||||
|
||||
const applicationName = 'Supabase'
|
||||
const faviconRoute = '/favicon'
|
||||
const faviconRoute = process.env.NODE_ENV === 'development' ? '/favicon/local' : '/favicon'
|
||||
const themeColor = DEFAULT_FAVICON_THEME_COLOR
|
||||
|
||||
// Advertise the .md version for AI agents on pages that have one.
|
||||
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Supabase",
|
||||
"short_name": "Supabase",
|
||||
"description": "The Postgres development platform.",
|
||||
"display": "standalone",
|
||||
"theme_color": "#1C1C1C",
|
||||
"background_color": "#1C1C1C",
|
||||
"icons": [
|
||||
{
|
||||
"src": "android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "android-icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
# Favicons
|
||||
|
||||
The vector sources are exported from [Favicons](https://www.figma.com/design/WCja3lpEj1DeunV1d5zu5D/Design-System?node-id=4691-1489). Each master is a native 512×512 Figma component with a 96px background radius and 60% corner smoothing, equivalent to 3px at 16px. The 16, 32 and 48px previews are scaled instances of these components. Scale the vectors, never a small raster export. Preserve the variant-specific bolt gradients, shadows and proportions.
|
||||
The vector sources are exported from [Favicons](https://www.figma.com/design/WCja3lpEj1DeunV1d5zu5D/Design-System?node-id=4691-1489). Each master is a native 512×512 Figma component with a 96px background radius and 60% corner smoothing, equivalent to 3px at 16px. The 16, 32 and 48px previews are scaled instances of these components. Scale the vectors, never a small raster export. Preserve the variant-specific bolt gradients, shadows, proportions and local inset edge detail.
|
||||
|
||||
| Source | Original node | Master node | Consumers |
|
||||
| ---------------- | ------------- | ----------- | ----------------------------------------------------------------- |
|
||||
| `production.svg` | `4691:1519` | `4696:14` | Studio production and self-hosted, WWW, UI Library, Design System |
|
||||
| `docs.svg` | `4691:1517` | `4696:15` | Docs, Learn, Knowledge Base |
|
||||
| `staging.svg` | `4691:1512` | `4696:17` | Hosted Studio non-production |
|
||||
| `local.svg` | `4691:1509` | `4696:19` | Local development in all apps, plus Studio CLI |
|
||||
|
||||
[Favicons (old)](https://www.figma.com/design/WCja3lpEj1DeunV1d5zu5D/Design-System?node-id=4431-5215) contains historical explorations, not interchangeable production assets.
|
||||
The local artwork also corresponds to the manually revised [512px reference](https://www.figma.com/design/WCja3lpEj1DeunV1d5zu5D/Design-System?node-id=4691-1483). [Favicons (old)](https://www.figma.com/design/WCja3lpEj1DeunV1d5zu5D/Design-System?node-id=4431-5215) contains historical explorations, not interchangeable production assets.
|
||||
|
||||
## Generation
|
||||
|
||||
The SVG backgrounds are flattened Figma vectors so 60% corner smoothing survives export.
|
||||
The SVG backgrounds are flattened Figma vectors so 60% corner smoothing survives export. Local corner grid marks are filled vectors, avoiding differences in dashed-stroke rendering.
|
||||
|
||||
From the repository root, after `pnpm install`:
|
||||
|
||||
@@ -49,11 +50,11 @@ copy.remove()
|
||||
- `android-icon-192x192.png` and `android-icon-512x512.png`: rounded artwork with transparent corners, manifest purpose `any`, not `maskable`.
|
||||
- `manifest.json`: relative icon URLs resolve beside the manifest under any application base path. Existing non-icon settings are retained.
|
||||
|
||||
Learn, UI Library and Design System use explicit layout declarations. Their `app/favicon.ico` copies are removed to avoid duplicate declarations. Shared declarations in `packages/common/MetaFavicons/icons.ts` serve Next.js pages/app routers, Studio TanStack and KB Astro. Applications select routes and detect their environments. Existing Studio environment routing is retained.
|
||||
Learn, UI Library and Design System use explicit layout declarations. Their `app/favicon.ico` copies are removed so Next.js does not also advertise production artwork during local development. Shared declarations in `packages/common/MetaFavicons/icons.ts` serve Next.js pages/app routers, Studio TanStack and KB Astro. Applications select routes and detect their environments. Next Studio retains its runtime CLI discovery; TanStack reads CLI mode on the server at request time and passes the favicon route through its root loader.
|
||||
|
||||
## Other artwork
|
||||
|
||||
WWW's historical Launch Week X favicon directory contains separate campaign artwork and remains unchanged. Its isolated legacy component has no current imports. Lite Studio keeps its independent production root ICO.
|
||||
WWW's historical Launch Week X favicon directory contains separate campaign artwork and remains unchanged. Its isolated legacy component has no current imports. Lite Studio keeps its independent production root ICO and explicitly selects the blue set during local development.
|
||||
|
||||
## Validation
|
||||
|
||||
@@ -62,3 +63,7 @@ node scripts/validate-favicons.mjs
|
||||
```
|
||||
|
||||
The script checks dimensions, opacity, ICO entries and manifest icon URLs under different base paths. Check for duplicate icon declarations in the rendered page head separately. Inspect 16px and 32px renders against light and dark browser chrome as well as larger masters.
|
||||
|
||||
## Local development
|
||||
|
||||
Next.js apps select `/favicon/local` when `NODE_ENV` is `development`; Astro and React Router use `import.meta.env.DEV`. Studio also recognises its explicit local environment and CLI mode. Production builds retain each app's normal branding. Manifest URLs follow the selected route and application base path.
|
||||
@@ -0,0 +1,17 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4700_24694)">
|
||||
<path d="M0 153.6C0 99.835 0 72.9524 10.4634 52.4169C19.6672 34.3534 34.3534 19.6672 52.4169 10.4634C72.9524 0 99.835 0 153.6 0H358.4C412.165 0 439.048 0 459.583 10.4634C477.647 19.6672 492.333 34.3534 501.537 52.4169C512 72.9524 512 99.835 512 153.6V358.4C512 412.165 512 439.048 501.537 459.583C492.333 477.647 477.647 492.333 459.583 501.537C439.048 512 412.165 512 358.4 512H153.6C99.835 512 72.9524 512 52.4169 501.537C34.3534 492.333 19.6672 477.647 10.4634 459.583C0 439.048 0 412.165 0 358.4V153.6Z" fill="#087BFF"/>
|
||||
<path d="M229.217 101.314C237.691 91.0185 254.887 96.6693 255.092 109.814L255.779 209.939H388.904C413.172 209.943 426.706 236.978 411.623 255.314L283.717 410.689C275.24 420.972 258.077 415.33 257.873 402.189L256.31 302.064H124.06C99.7881 302.064 86.2529 275.027 101.342 256.689L229.217 101.314Z" fill="white" fill-opacity="0.7"/>
|
||||
<path d="M229.217 101.314C237.691 91.0185 254.887 96.6693 255.092 109.814L255.779 209.939H388.904C413.172 209.943 426.706 236.978 411.623 255.314L283.717 410.689C275.24 420.972 258.077 415.33 257.873 402.189L256.31 302.064H124.06C99.7881 302.064 86.2529 275.027 101.342 256.689L229.217 101.314Z" fill="url(#paint0_linear_4700_24694)" fill-opacity="0.9"/>
|
||||
<path opacity="0.3" d="M64 343.528H92V396.8C92 397.856 92.0003 398.869 92.001 399.851L92.0117 416.451L92.583 418.082L93.0742 418.925L93.917 419.416L95.5479 419.987L112.149 419.999C113.131 420 114.144 420 115.2 420H140.472V448H115.2L108.886 447.993C95.0714 447.945 87.4621 447.564 81.4727 444.513C75.4515 441.445 70.5553 436.549 67.4873 430.527C63.9998 423.682 64 414.721 64 396.8V343.528ZM168.472 420V448H140.472V420H168.472ZM371.528 420V448H343.528V420H371.528ZM448 396.8C448 414.721 448 423.682 444.513 430.527C441.445 436.549 436.549 441.445 430.527 444.513C423.682 448 414.721 448 396.8 448H371.528V420H396.8C397.856 420 398.869 420 399.851 419.999L416.451 419.987L418.082 419.416L418.925 418.925L419.416 418.082L419.987 416.451L419.999 399.851C420 398.869 420 397.856 420 396.8V343.528H448V396.8ZM64 115.2C64 97.2789 63.9998 88.3178 67.4873 81.4727C70.5553 75.4515 75.4515 70.5553 81.4727 67.4873C87.4621 64.4357 95.0714 64.0545 108.886 64.0068L115.2 64H168.472V92H115.2C114.144 92 113.131 92.0003 112.149 92.001L95.5479 92.0117L93.917 92.583L93.0742 93.0742L92.583 93.917L92.0117 95.5479L92.001 112.149C92.0003 113.131 92 114.144 92 115.2V168.472H64V115.2ZM448 168.472H420V115.2C420 114.144 420 113.131 419.999 112.149L419.987 95.5479L419.416 93.917L418.925 93.0742L418.082 92.583L416.451 92.0117L399.851 92.001C398.869 92.0003 397.856 92 396.8 92H343.528V64H396.8C414.721 64 423.682 63.9998 430.527 67.4873C436.549 70.5553 441.445 75.4515 444.513 81.4727C448 88.3178 448 97.2789 448 115.2V168.472Z" fill="#EDEDED" fill-opacity="0.8"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4700_24694" x1="-0.999484" y1="0.00105359" x2="510.995" y2="511.998" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.331731" stop-color="white"/>
|
||||
<stop offset="0.673077" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_4700_24694">
|
||||
<rect width="512" height="512" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -1,6 +1,9 @@
|
||||
export const faviconTargets = [
|
||||
['production', 'apps/studio/public/favicon'],
|
||||
['staging', 'apps/studio/public/favicon/staging'],
|
||||
...['studio', 'www', 'docs', 'learn', 'ui-library', 'design-system', 'kb', 'lite-studio'].map(
|
||||
(app) => ['local', `apps/${app}/public/favicon/local`]
|
||||
),
|
||||
...['www', 'ui-library', 'design-system'].map((app) => [
|
||||
'production',
|
||||
`apps/${app}/public/favicon`,
|
||||
|
||||
@@ -32,7 +32,7 @@ for (const [variant, relativeDirectory] of faviconTargets) {
|
||||
const directory = path.join(root, relativeDirectory)
|
||||
await mkdir(directory, { recursive: true })
|
||||
const svg = await readFile(path.join(root, `assets/favicons/${variant}.svg`))
|
||||
const background = variant === 'staging' ? '#FFFFFF' : '#1C1C1C'
|
||||
const background = variant === 'staging' ? '#FFFFFF' : variant === 'local' ? '#087BFF' : '#1C1C1C'
|
||||
const render = (size) => sharp(svg).resize(size, size)
|
||||
const images = await Promise.all(
|
||||
[16, 32, 48].map(async (size) => ({ size, data: await render(size).png().toBuffer() }))
|
||||
@@ -60,4 +60,4 @@ for (const [variant, relativeDirectory] of faviconTargets) {
|
||||
}
|
||||
|
||||
// Next.js layouts declare their icons explicitly. Do not add app/favicon.ico,
|
||||
// which would duplicate the shared icon declarations.
|
||||
// which would also advertise the production artwork during local development.
|
||||
@@ -14,6 +14,10 @@ for (const [variant, directory] of faviconTargets) {
|
||||
const svg = await readFile(path.join(root, `assets/favicons/${variant}.svg`), 'utf8')
|
||||
assert.match(svg, /width="512" height="512"/)
|
||||
assert.match(svg, /M0 153\.6C0 99\.835/)
|
||||
if (variant === 'local') {
|
||||
assert.doesNotMatch(svg, /stroke-dasharray|stroke-width/)
|
||||
assert.match(svg, /<path opacity="0.3"/)
|
||||
}
|
||||
const ico = await readFile(file('favicon.ico'))
|
||||
assert.equal(ico.readUInt16LE(0), 0)
|
||||
assert.equal(ico.readUInt16LE(2), 1)
|
||||
|
||||