TinyKit Docs

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

  1. Site Header (/src/app/(root)/_nav/Header.tsx):

    • Displays in main navigation
    • Falls back to text-based site name if no logo
    • Uses priority loading for immediate display
  2. 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
  3. Landing Page Header:

    • Same header component as site header
    • Consistent branding across all pages

1. Navigate to Site Settings

  1. Sign in as admin
  2. Go to /admin/site-settings
  3. Click on the "Site Logo" tab
  1. Click "Upload Site Logo" or drag and drop
  2. Select your SVG or PNG file (max 2MB)
  3. Crop/adjust if needed (4:1 aspect ratio recommended)
  4. 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

  1. Use SVG for scalability:

    • Vector graphics scale perfectly at any size
    • Smaller file size than PNG
    • Better for retina displays
  2. Follow 4:1 aspect ratio:

    • Horizontal logos work best in headers
    • Recommended: 400x100px
    • Ensures consistent display across devices
  3. Keep it simple:

    • Logo should be legible at small sizes
    • Avoid intricate details that don't scale
    • Test at different sizes before uploading
  4. 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

  1. Check Convex dev server is running:

    pnpm dev
    # or
    npx convex dev
  2. Verify function is deployed:

    • New getLogoUrl query must be picked up by Convex
    • Restart dev server if needed
  3. Check browser console:

    • Look for 404 errors on logo URL
    • Verify storage URL is valid

Upload fails

  1. Check file size: Must be <2MB
  2. Check file format: Only SVG and PNG supported
  3. Check permissions: Must be admin
  4. Check Convex storage: Verify storage is configured

Logo looks corrupted

  1. Ensure unoptimized prop is used:

    <Image src={logoUrl} unoptimized />
  2. For SVG: Check SVG is valid and well-formed

  3. For PNG: Verify transparency is preserved

On this page

Ship your startup faster. In minutes.

Get TinyKit SaaS