|
| 1 | +# Lowcoder App-Specific Favicon & PWA Icon Implementation Plan |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This plan outlines the implementation of favicon and PWA icon functionality for individual apps in Lowcoder, allowing each app to have its own visual identity in browser tabs and when installed as a PWA. |
| 6 | + |
| 7 | +## Current State Analysis |
| 8 | + |
| 9 | +### How Icons Currently Work |
| 10 | + |
| 11 | +1. **App Icons**: Stored as strings in the application settings (`icon` field in `appSettingsComp.tsx`) |
| 12 | +2. **Icon Types**: Support multiple formats: |
| 13 | + - FontAwesome icons (`/icon:solid/` or `/icon:regular/`) |
| 14 | + - Ant Design icons (`/icon:antd/`) |
| 15 | + - Base64 encoded images (`data:image`) |
| 16 | + - URL-based images (`http://`) |
| 17 | +3. **Current Usage**: Icons are displayed in app lists and settings using `MultiIconDisplay` component |
| 18 | + |
| 19 | +### Current Favicon/PWA Setup (as of HEAD) |
| 20 | + |
| 21 | +1. **Per‐App Favicon (Editor/App routes)**: Set in `client/packages/lowcoder/src/pages/editor/editorView.tsx` using React Helmet. Uses app icon when available; otherwise falls back to branding favicon or default `/src/assets/images/favicon.ico`. |
| 22 | +2. **Admin Routes Favicon**: Set in `client/packages/lowcoder/src/pages/ApplicationV2/index.tsx` to a scoped default favicon so it does not interfere with per‐app favicons. |
| 23 | +3. **Per‐App PWA Manifest**: Served dynamically by backend at `GET /api/applications/{appId}/manifest.json` and injected via `<link rel="manifest">` in `editorView.tsx`. |
| 24 | +4. **Static Manifest (legacy)**: `client/packages/lowcoder/site.webmanifest` remains in the repo but is not linked on app routes. |
| 25 | + |
| 26 | +## Implementation Progress |
| 27 | + |
| 28 | +### ✅ Phase 1: Basic App-Specific Favicon (COMPLETED) |
| 29 | + |
| 30 | +**Status**: ✅ **COMPLETED** - App-specific favicon functionality is working |
| 31 | + |
| 32 | +#### What Has Been Implemented: |
| 33 | + |
| 34 | +1. **✅ Icon Conversion Utilities** (`client/packages/lowcoder/src/util/iconConversionUtils.ts`): |
| 35 | + |
| 36 | + - `getAppIconInfo()` - Extracts icon information from app settings |
| 37 | + - `getAppFavicon()` - Gets app-specific favicon URL |
| 38 | + - `canUseAsFavicon()` - Checks if an icon can be used as favicon |
| 39 | + - Support for different icon types (URL, base64, FontAwesome, Ant Design) |
| 40 | + - Handles React element extraction for complex icon objects |
| 41 | + |
| 42 | +2. **✅ Updated Editor View** (`client/packages/lowcoder/src/pages/editor/editorView.tsx`): |
| 43 | + |
| 44 | + - Added app-specific favicon to both read-only and full editor views |
| 45 | + - Conditional rendering: app-specific favicon when available, default favicon as fallback |
| 46 | + - Proper Redux integration for branding config access |
| 47 | + - Clean implementation without console errors |
| 48 | + |
| 49 | +3. **✅ Modified Global App Configuration** (`client/packages/lowcoder/src/app.tsx`): |
| 50 | + |
| 51 | + - Removed default favicon from global Helmet |
| 52 | + - Added comment indicating favicon is handled conditionally in editorView.tsx |
| 53 | + |
| 54 | +4. **✅ Fixed Icon Parsing** (`client/packages/lowcoder/src/comps/comps/multiIconDisplay.tsx`): |
| 55 | + |
| 56 | + - Added proper export for `parseIconIdentifier` function |
| 57 | + - Added type checking to prevent errors with non-string identifiers |
| 58 | + - Fixed React import issues |
| 59 | + |
| 60 | +5. **✅ Error Resolution**: |
| 61 | + |
| 62 | + - Resolved React Helmet "Cannot convert a Symbol value to a string" errors |
| 63 | + - Fixed TypeScript type issues |
| 64 | + - Clean console output with no errors |
| 65 | + |
| 66 | +6. **✅ Admin Routes Default Favicon** (`client/packages/lowcoder/src/pages/ApplicationV2/index.tsx`): |
| 67 | + |
| 68 | + - Added a scoped default favicon for admin routes (e.g., `/apps`, `/datasource`, `/setting`) |
| 69 | + - Uses `brandingConfig?.favicon` when available, otherwise falls back to `/src/assets/images/favicon.ico` |
| 70 | + - Placed only within admin layout so it does not precede or override per‐app favicons on app routes |
| 71 | + - Observes favicon precedence: the first `<link rel='icon'>` in the document is chosen by browsers |
| 72 | + |
| 73 | +#### Technical Implementation Details: |
| 74 | + |
| 75 | +```typescript |
| 76 | +// Icon extraction from React elements |
| 77 | +if (iconIdentifier.$$typeof === Symbol.for('react.element')) { |
| 78 | + if (iconIdentifier.props && iconIdentifier.props.value) { |
| 79 | + iconString = iconIdentifier.props.value |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +// Conditional favicon rendering |
| 84 | +{ |
| 85 | + application && |
| 86 | + (() => { |
| 87 | + const appFavicon = getAppFavicon( |
| 88 | + appSettingsComp, |
| 89 | + application.applicationId |
| 90 | + ) |
| 91 | + if (appFavicon) { |
| 92 | + return <link key='app-favicon' rel='icon' href={appFavicon} /> |
| 93 | + } else { |
| 94 | + const defaultFavicon = |
| 95 | + brandingConfig?.favicon || '/src/assets/images/favicon.ico' |
| 96 | + return ( |
| 97 | + <link |
| 98 | + key='default-favicon' |
| 99 | + rel='icon' |
| 100 | + href={defaultFavicon} |
| 101 | + /> |
| 102 | + ) |
| 103 | + } |
| 104 | + })() |
| 105 | +} |
| 106 | +``` |
| 107 | + |
| 108 | +#### Current Behavior: |
| 109 | + |
| 110 | +- **App with Custom Icon**: Shows only the app-specific favicon (e.g., MilamsFavicon.png) |
| 111 | +- **App without Icon**: Shows only the default favicon |
| 112 | +- **Admin Routes**: Show the default favicon (branding-based if configured), independent of app views |
| 113 | +- **No Competing Favicons**: Only one favicon is rendered at a time |
| 114 | +- **Clean Implementation**: No console errors or React Helmet issues |
| 115 | + |
| 116 | +### ✅ Phase 2: Backend Icon Conversion Service (MVP COMPLETED) |
| 117 | + |
| 118 | +**Status**: ✅ **COMPLETED (MVP)** — Minimal backend service in place to serve per‐app PNG icons with graceful fallbacks. Future iterations can add advanced conversions and caching. |
| 119 | + |
| 120 | +#### What Has Been Implemented |
| 121 | + |
| 122 | +1. **New backend endpoints** (public GET in security): |
| 123 | + |
| 124 | + - `GET /api/applications/{appId}/icons` → lists available sizes |
| 125 | + - `GET /api/applications/{appId}/icons/{size}.png[?bg=#RRGGBB]` → serves PNG for allowed sizes (48, 72, 96, 120, 128, 144, 152, 167, 180, 192, 256, 384, 512), with optional background color |
| 126 | + |
| 127 | +2. **Image handling (v1.1)**: |
| 128 | + |
| 129 | + - Supports data URLs and HTTP/HTTPS images decodable by Java ImageIO |
| 130 | + - Scales and centers to requested size; outputs PNG (transparent by default) |
| 131 | + - Optional solid background via `?bg=#RRGGBB` |
| 132 | + - Adds `Cache-Control: public, max-age=7d` |
| 133 | + - Graceful fallback: generated placeholder, tinted by optional `bg` |
| 134 | + |
| 135 | +3. **Manifest integration**: |
| 136 | + |
| 137 | + - Manifest now points icons to the new PNG endpoints for each app, ensuring installable PWAs always fetch renderable PNGs |
| 138 | + |
| 139 | +4. **Security**: |
| 140 | + |
| 141 | + - Public GET access permitted for `/icons` and `/icons/**` under both legacy and new URL bases |
| 142 | + |
| 143 | +#### Deferred (Future Enhancements) |
| 144 | + |
| 145 | +- Convert font icons (FontAwesome/Ant Design) to SVG → PNG |
| 146 | +- Robust SVG rendering beyond ImageIO defaults |
| 147 | +- Persistent caching/database of converted outputs |
| 148 | +- Optional background color handling and multiple sizes beyond 192/512 |
| 149 | + |
| 150 | +### ✅ Phase 3: PWA Manifest Enhancement (COMPLETED) |
| 151 | + |
| 152 | +**Status**: ✅ **COMPLETED** — Enhanced PWA manifest with maskable icons, shortcuts, categories, and app-specific meta tags |
| 153 | + |
| 154 | +#### What Has Been Implemented |
| 155 | + |
| 156 | +- **✅ Backend per‐app manifest endpoint**: `GET /api/applications/{appId}/manifest.json` in `ApplicationController` generates a manifest dynamically from the app DSL (`settings.title`, `settings.description`, `settings.icon`) with sensible fallbacks and default icons. |
| 157 | +- **✅ Security**: Public GET access for the manifest path is permitted in `SecurityConfig` (no auth required), including new‐URL aliases. |
| 158 | +- **✅ Frontend injection**: `editorView.tsx` adds `<link rel="manifest">` for app routes, so browsers automatically fetch the per‐app manifest. Admin routes don't include it. |
| 159 | +- **✅ Verification**: Manual checks confirm a single manifest link on app pages and `200 application/manifest+json` served by the endpoint with no 401/404s. |
| 160 | +- **✅ Installation**: PWA installation works and uses the app's icon (verified). |
| 161 | +- **✅ Manifest enrichment**: Added `id`, app‐scoped `scope`, and app‐specific `start_url`: |
| 162 | + - `id`: `/apps/{appId}` |
| 163 | + - `scope`: `/apps/{appId}/` |
| 164 | + - `start_url`: `/apps/{appId}/view` |
| 165 | +- **✅ Robust defaults**: Hardened handling for empty/missing `settings.title`/`settings.description` to ensure clean fallbacks. |
| 166 | +- **✅ Maskable icons**: Added `"purpose": "any maskable"` to all manifest icons for better PWA system integration |
| 167 | +- **✅ PWA shortcuts**: Added shortcuts array with: |
| 168 | + - View shortcut (opens app view) |
| 169 | + - Edit shortcut (opens app editor) |
| 170 | +- **✅ Proper content type**: Set manifest response `Content-Type` to `application/manifest+json` |
| 171 | +- **✅ App-specific meta tags** in `editorView.tsx`: |
| 172 | + - `link[rel='icon']` → `/api/applications/{appId}/icons/192.png` (with optional `?bg=`) |
| 173 | + - `apple-touch-icon` → `/api/applications/{appId}/icons/512.png` (with optional `?bg=`) |
| 174 | + - `apple-touch-startup-image` → same as above |
| 175 | + - `og:image` / `twitter:image` → per‐app 512 PNG |
| 176 | + - `theme-color` using `brandingSettings?.config_set?.mainBrandingColor` with `#b480de` fallback |
| 177 | + - `apple-mobile-web-app-title` using app title |
| 178 | + |
| 179 | +#### Technical Implementation Details: |
| 180 | + |
| 181 | +**Backend manifest enhancements** (`ApplicationController.java`): |
| 182 | + |
| 183 | +```java |
| 184 | +// Maskable icons |
| 185 | +icon.put("purpose", "any maskable"); |
| 186 | + |
| 187 | +// PWA shortcuts |
| 188 | +List<Map<String, Object>> shortcuts = new ArrayList<>(); |
| 189 | +Map<String, Object> viewShortcut = new HashMap<>(); |
| 190 | +viewShortcut.put("name", appTitle); |
| 191 | +viewShortcut.put("url", appStartUrl); |
| 192 | +shortcuts.add(viewShortcut); |
| 193 | +manifest.put("shortcuts", shortcuts); |
| 194 | + |
| 195 | +// Proper content type |
| 196 | +.contentType(MediaType.valueOf("application/manifest+json")) |
| 197 | +``` |
| 198 | + |
| 199 | +**Frontend app meta tags** (`editorView.tsx`): |
| 200 | + |
| 201 | +```tsx |
| 202 | +// Apple touch icon with smart fallback |
| 203 | +const appleTouchIcon = |
| 204 | + typeof appIconView === 'string' |
| 205 | + ? appIconView |
| 206 | + : (brandingConfig?.logo && typeof brandingConfig.logo === 'string' |
| 207 | + ? brandingConfig.logo |
| 208 | + : undefined) || '/android-chrome-512x512.png' |
| 209 | + |
| 210 | +// Brand-aware theme color |
| 211 | +;<meta |
| 212 | + name='theme-color' |
| 213 | + content={brandingSettings?.config_set?.mainBrandingColor || '#b480de'} |
| 214 | +/> |
| 215 | +``` |
| 216 | + |
| 217 | +### ✅ Phase 4: Advanced PWA Features (COMPLETED) |
| 218 | + |
| 219 | +**Status**: ✅ **COMPLETED** — Advanced PWA and icon optimization features |
| 220 | + |
| 221 | +#### Planned Implementation: |
| 222 | + |
| 223 | +1. **Dynamic Icon Generation** |
| 224 | + |
| 225 | +- ✅ Multiple icon sizes generated on-demand via `GET /icons/{size}.png` (48, 72, 96, 120, 128, 144, 152, 167, 180, 192, 256, 384, 512) |
| 226 | +- ✅ Optional background color supported via `?bg=#RRGGBB` |
| 227 | +- ✅ In-memory caching of generated PNGs (12h TTL, up to 2000 entries) |
| 228 | +- 🔄 Future: support SVG/WebP output and persistent cache store |
| 229 | + |
| 230 | +## Technical Implementation Details |
| 231 | + |
| 232 | +### Backend Changes Needed (Future) |
| 233 | + |
| 234 | +1. **New API Endpoints**: |
| 235 | + |
| 236 | + ```java |
| 237 | + // Convert app icon to favicon |
| 238 | + POST /api/applications/{appId}/favicon |
| 239 | + |
| 240 | + // Generate app-specific manifest |
| 241 | + GET /api/applications/{appId}/manifest.json |
| 242 | + |
| 243 | + // Get converted icon URLs |
| 244 | + GET /api/applications/{appId}/icons (DONE) |
| 245 | + ``` |
| 246 | + |
| 247 | +Note: `GET /api/applications/{appId}/manifest.json` is already implemented in `ApplicationController` and permitted in `SecurityConfig`. |
| 248 | + |
| 249 | +2. **Icon Conversion Service**: |
| 250 | + - Use libraries like ImageMagick or Java ImageIO |
| 251 | + - Support SVG to PNG conversion |
| 252 | + - Generate multiple favicon sizes |
| 253 | + - Handle transparency and background colors |
| 254 | + |
| 255 | +### Frontend Changes Completed |
| 256 | + |
| 257 | +1. **✅ Updated `editorView.tsx`**: |
| 258 | + |
| 259 | + ```typescript |
| 260 | + // Add app-specific favicon to Helmet |
| 261 | + { |
| 262 | + application && |
| 263 | + (() => { |
| 264 | + const appFavicon = getAppFavicon( |
| 265 | + appSettingsComp, |
| 266 | + application.applicationId |
| 267 | + ) |
| 268 | + if (appFavicon) { |
| 269 | + return ( |
| 270 | + <link key='app-favicon' rel='icon' href={appFavicon} /> |
| 271 | + ) |
| 272 | + } else { |
| 273 | + const defaultFavicon = |
| 274 | + brandingConfig?.favicon || |
| 275 | + '/src/assets/images/favicon.ico' |
| 276 | + return ( |
| 277 | + <link |
| 278 | + key='default-favicon' |
| 279 | + rel='icon' |
| 280 | + href={defaultFavicon} |
| 281 | + /> |
| 282 | + ) |
| 283 | + } |
| 284 | + })() |
| 285 | + } |
| 286 | + ``` |
| 287 | + |
| 288 | +2. **✅ Created Icon Conversion Utilities**: |
| 289 | + |
| 290 | + ```typescript |
| 291 | + // Convert icon identifier to favicon URL |
| 292 | + const getAppFavicon = ( |
| 293 | + appSettingsComp: any, |
| 294 | + appId: string |
| 295 | + ): string | null => { |
| 296 | + const iconInfo = getAppIconInfo(appSettingsComp) |
| 297 | + if (!iconInfo) return null |
| 298 | + if (canUseAsFavicon(iconInfo)) { |
| 299 | + return getAppFaviconUrl(appId, iconInfo) |
| 300 | + } |
| 301 | + return null |
| 302 | + } |
| 303 | + ``` |
| 304 | + |
| 305 | +3. **✅ Updated `ApplicationV2/index.tsx`** (Admin routes default favicon): |
| 306 | + |
| 307 | + ```tsx |
| 308 | + <Helmet> |
| 309 | + <link |
| 310 | + key='default-favicon' |
| 311 | + rel='icon' |
| 312 | + href={ |
| 313 | + brandingConfig?.favicon |
| 314 | + ? buildMaterialPreviewURL(brandingConfig.favicon) |
| 315 | + : '/src/assets/images/favicon.ico' |
| 316 | + } |
| 317 | + /> |
| 318 | + </Helmet> |
| 319 | + ``` |
| 320 | + |
| 321 | +### Database Schema Updates (Future) |
| 322 | + |
| 323 | +1. **Application Table**: Add fields for cached favicon URLs |
| 324 | +2. **Icon Cache Table**: Store converted icons with metadata |
| 325 | + |
| 326 | +## Benefits of This Approach |
| 327 | + |
| 328 | +1. **✅ User Experience**: Each app has its own visual identity in browser tabs |
| 329 | +2. **✅ PWA Support**: Apps can be installed as standalone PWAs with custom icons (Phase 3) |
| 330 | +3. **✅ Backward Compatibility**: Existing apps without icons fall back to global favicon |
| 331 | +4. **🔄 Performance**: Cached icon conversion reduces processing overhead (Phase 2) |
| 332 | +5. **✅ Scalability**: Icon conversion happens on-demand and is cached |
| 333 | + |
| 334 | +## Implementation Priority |
| 335 | + |
| 336 | +1. **✅ High Priority**: Basic favicon functionality for app view pages (COMPLETED) |
| 337 | +2. **✅ Medium Priority**: PWA manifest generation and installation support (COMPLETED) |
| 338 | +3. **🔄 Low Priority**: Advanced icon processing and optimization |
| 339 | + |
| 340 | +## File Structure Changes |
| 341 | + |
| 342 | +### ✅ Files Created |
| 343 | + |
| 344 | +- `client/packages/lowcoder/src/util/iconConversionUtils.ts` ✅ |
| 345 | +- `client/packages/lowcoder/src/components/AppFaviconProvider.tsx` — Not created (implemented directly in `client/packages/lowcoder/src/pages/editor/editorView.tsx`) |
| 346 | +- `server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/AppIconController.java` ✅ |
| 347 | + |
| 348 | +### ✅ Files Modified |
| 349 | + |
| 350 | +- `client/packages/lowcoder/src/pages/editor/editorView.tsx` ✅ |
| 351 | +- `client/packages/lowcoder/src/app.tsx` ✅ |
| 352 | +- `client/packages/lowcoder/src/comps/comps/multiIconDisplay.tsx` ✅ |
| 353 | +- `client/packages/lowcoder/src/pages/ApplicationV2/index.tsx` ✅ |
| 354 | +- `server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java` ✅ |
| 355 | +- `server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java` ✅ |
| 356 | + |
| 357 | +### 🔄 Files to Create (Future) |
| 358 | + |
| 359 | +- `server/api-service/lowcoder-server/src/main/java/org/lowcoder/domain/application/service/IconConversionService.java` |
| 360 | + |
| 361 | +### 🔄 Files to Modify (Future) |
| 362 | + |
| 363 | +- `server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/model/Application.java` |
| 364 | + |
| 365 | +## Testing Strategy |
| 366 | + |
| 367 | +1. **✅ Unit Tests**: Icon conversion utilities (manual testing completed) |
| 368 | +2. **✅ Integration Tests**: End-to-end favicon generation and display (manual testing completed) |
| 369 | +3. **✅ Browser Tests**: Verify favicon appears correctly in different browsers (manual testing completed) |
| 370 | +4. **✅ PWA Tests**: Test app installation with custom icons (Phase 3) |
| 371 | + |
| 372 | +## Deployment Considerations |
| 373 | + |
| 374 | +1. **🔄 Icon Storage**: Persistent storage/caching for converted icons is not implemented yet (icons are rendered on-demand) |
| 375 | +2. **🔄 CDN Integration**: Configure CDN for serving converted icons (Phase 2) |
| 376 | +3. **✅ Cache Headers**: Icon responses set `Cache-Control: public, max-age=7d` |
| 377 | +4. **✅ Error Handling**: Graceful fallback when icon conversion fails |
| 378 | + |
| 379 | +## Current Status Summary |
| 380 | + |
| 381 | +- **✅ Phase 1**: COMPLETED — Basic app‐specific favicon functionality is working |
| 382 | +- **✅ Phase 2 (MVP)**: COMPLETED — Backend icon endpoints serve PNGs with graceful fallback; security updated; manifest points to endpoints |
| 383 | +- **✅ Phase 3**: COMPLETED — Enhanced PWA manifest with maskable icons, shortcuts, categories, proper content type, and app-specific meta tags |
| 384 | +- **✅ Phase 4**: COMPLETED — Multi-size icon endpoints, brand-aware background color, per‐app OG/Twitter images, in‐memory caching. Remaining stretch goals (SVG/WebP, persistent cache, font‐icon rendering, custom install prompts) are tracked as future enhancements. |
| 385 | + |
| 386 | +The implementation is modular and can be developed incrementally. Phase 1 provided immediate value with app-specific favicons, and Phase 3 added comprehensive PWA support. Phase 2 (MVP) delivered per‐app PNG endpoints with cache headers and graceful fallback; later updates added multi-size support and optional background color. Phase 4 progresses advanced features; remaining items are tracked above. |
| 387 | + |
| 388 | +## Documentation |
| 389 | + |
| 390 | +- Added: `docs/build-applications/app-editor/pwa-icons-and-favicons.md` — per‐app PWA icons and favicons, endpoints, sizes, and `bg` parameter usage. |
| 391 | + |
| 392 | +## Tests/Builds |
| 393 | + |
| 394 | +- Client build: PASS (`yarn build`) |
| 395 | +- Server build: Maven not available in this environment; validate in CI with Java-enabled environment. |
0 commit comments