Site Logo Management
Location: /admin/site-settings → Site Logo tab
Access: Admin only
Supported Formats: SVG (recommended), PNG
Max File Size: 2MB
Recommended Dimensions: 400x100px (4:1 aspect ratio)
Overview
TinyKit SaaS includes a comprehensive site logo management system that allows admins to upload and manage the site's logo. The logo is displayed in multiple locations across the application and is optimized for both performance and quality.
Features
- SVG & PNG Support: Upload vector (SVG) or raster (PNG) logos
- Automatic Storage Management: Old logos are automatically deleted when updating
- Unoptimized Image Display: Logos use Next.js
<Image unoptimized />to prevent SVG corruption - Multi-location Display: Logo appears in headers, OG images, and landing pages
- Preview Before Upload: See how your logo will look before saving
- One-Click Removal: Easy logo removal with automatic storage cleanup
Logo Display Locations
-
Site Header (
/src/app/(root)/_nav/Header.tsx):- Displays in main navigation
- Falls back to text-based site name if no logo
- Uses
priorityloading for immediate display
-
OG Images (
/src/components/utils/OGImageGenerator.tsx):- Appears in social media preview images
- Automatically included in generated Open Graph images
- Falls back to site name if no logo
-
Landing Page Header:
- Same header component as site header
- Consistent branding across all pages
How to Upload a Logo
1. Navigate to Site Settings
- Sign in as admin
- Go to
/admin/site-settings - Click on the "Site Logo" tab
2. Upload Your Logo
- Click "Upload Site Logo" or drag and drop
- Select your SVG or PNG file (max 2MB)
- Crop/adjust if needed (4:1 aspect ratio recommended)
- Click "Save" to upload
3. Preview and Confirm
- Logo preview appears immediately after upload
- Check the header to see your logo in action
- Generate an OG image to see social media preview
Technical Implementation
Database Schema
// convex/siteSettings/schema.ts
siteSettings: defineTable({
// ... other fields
logoStorageId: v.optional(v.id("_storage")), // Site logo storage reference
});Backend Functions
Upload URL Generation (Admin only):
// convex/siteSettings/private/mutations.ts
export const generateLogoUploadUrl = mutation({
args: {},
handler: async (ctx) => {
await requireAccess(ctx, { userRole: ["admin"] });
return await ctx.storage.generateUploadUrl();
},
});Logo Update (Automatic cleanup):
export const updateLogoId = mutation({
args: { logoStorageId: v.id("_storage") },
handler: async (ctx, { logoStorageId }) => {
const { userId } = await requireAccess(ctx, { userRole: ["admin"] });
const settings = await ctx.db.query("siteSettings").first();
// Delete old logo if exists
if (settings?.logoStorageId) {
await ctx.storage.delete(settings.logoStorageId);
}
// Save new logo
await ctx.db.patch(settings._id, { logoStorageId });
},
});Logo Removal:
export const removeLogoImage = mutation({
args: {},
handler: async (ctx) => {
const { userId } = await requireAccess(ctx, { userRole: ["admin"] });
const settings = await ctx.db.query("siteSettings").first();
// Remove from database
await ctx.db.patch(settings._id, { logoStorageId: undefined });
// Delete from storage
if (settings?.logoStorageId) {
await ctx.storage.delete(settings.logoStorageId);
}
},
});Frontend Display
Header Component (with unoptimized prop):
// src/app/(root)/_nav/Header.tsx
import Image from "next/image";
export default function Header() {
const { settings } = useSettingsWithState();
return (
<header>
<Link href="/">
{settings?.logoUrl ? (
<Image
src={settings.logoUrl}
alt={settings?.siteName ?? "Logo"}
width={120}
height={40}
unoptimized // CRITICAL: Prevents Next.js optimization for SVG/PNG
className="max-h-10 w-auto"
priority // Load logo immediately
/>
) : (
<h1>{settings?.siteName ?? "TinySaaS"}</h1>
)}
</Link>
</header>
);
}Why unoptimized?
- SVG files don't benefit from Next.js image optimization
- Small PNG logos (<1KB) are faster without optimization
- Prevents potential corruption of vector graphics
- Ensures logos display exactly as uploaded
OG Image Integration
// src/components/utils/OGImageGenerator.tsx
export async function generateOGImage() {
const settings = await fetchSettings();
return new ImageResponse(
<div>
{settings.logoUrl && (
<img
src={settings.logoUrl}
alt="Logo"
style={{ height: 100, width: "auto" }}
/>
)}
<div>{settings.siteName}</div>
</div>
);
}Best Practices
Logo Design
-
Use SVG for scalability:
- Vector graphics scale perfectly at any size
- Smaller file size than PNG
- Better for retina displays
-
Follow 4:1 aspect ratio:
- Horizontal logos work best in headers
- Recommended: 400x100px
- Ensures consistent display across devices
-
Keep it simple:
- Logo should be legible at small sizes
- Avoid intricate details that don't scale
- Test at different sizes before uploading
-
Consider dark mode:
- Single logo for both light and dark themes
- Use transparent backgrounds
- Ensure contrast works in both modes
File Size Optimization
For SVG:
- Use SVGO or similar tools to optimize
- Remove unnecessary metadata
- Combine paths where possible
- Target: <50KB for fast loading
For PNG:
- Use TinyPNG or similar compression
- Export at 2x resolution for retina
- Use PNG-8 if possible (limited colors)
- Target: <100KB for fast loading
Admin UI Component
SiteLogoCard (/src/components/tinykit/features/admin/site-settings/SiteLogoCard.tsx):
export function SiteLogoCard() {
const generateUploadUrl = useMutation(
api.siteSettings.private.mutations.generateLogoUploadUrl
);
const updateLogoId = useMutation(
api.siteSettings.private.mutations.updateLogoId
);
const removeLogoImage = useMutation(
api.siteSettings.private.mutations.removeLogoImage
);
const logoUrl = useQuery(api.siteSettings.public.queries.getLogoUrl);
return (
<Card>
<CardHeader>
<CardTitle>Site Logo</CardTitle>
</CardHeader>
<CardContent>
{/* Current Logo Preview */}
{logoUrl && (
<Image
src={logoUrl}
alt="Site Logo"
width={400}
height={100}
unoptimized
className="max-h-24 w-auto"
/>
)}
{/* Upload Component */}
<ImageUpload
generateUploadUrl={generateUploadUrl}
onImageUploaded={handleImageUploaded}
onImageRemoved={handleRemoveImage}
aspectRatio={4 / 1}
targetWidth={400}
targetHeight={100}
maxSize={2}
outputFormat="png"
quality={0.9}
/>
</CardContent>
</Card>
);
}Public API
Get Logo URL (No authentication required):
// convex/siteSettings/public/queries.ts
export const getLogoUrl = query({
args: {},
handler: async (ctx) => {
const settings = await ctx.db.query("siteSettings").first();
if (!settings?.logoStorageId) {
return null;
}
return await ctx.storage.getUrl(settings.logoStorageId);
},
});HTTP Endpoint:
GET /og-settings
Response:
{
"siteName": "TinySaaS",
"slogan": "Tiny Tasks, Massive Results",
"description": "Modern collaboration platform...",
"logoUrl": "https://convex-storage.../logo.svg",
"primaryColor": "#9333ea",
"secondaryColor": "#dfd9ec",
"fontColor": "#3d3c4f"
}Troubleshooting
Logo not displaying
-
Check Convex dev server is running:
pnpm dev # or npx convex dev -
Verify function is deployed:
- New
getLogoUrlquery must be picked up by Convex - Restart dev server if needed
- New
-
Check browser console:
- Look for 404 errors on logo URL
- Verify storage URL is valid
Upload fails
- Check file size: Must be <2MB
- Check file format: Only SVG and PNG supported
- Check permissions: Must be admin
- Check Convex storage: Verify storage is configured
Logo looks corrupted
-
Ensure
unoptimizedprop is used:<Image src={logoUrl} unoptimized /> -
For SVG: Check SVG is valid and well-formed
-
For PNG: Verify transparency is preserved
Related Documentation
- Site Branding - Overall branding configuration
- OG Images - Social media image generation
- Authorization - Access control and permissions