> ## Documentation Index
> Fetch the complete documentation index at: https://www.logo.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Add company logos to job listings

> Give every listing an employer logo that loads from the domain on the record. Long feeds stay clean even when a company has no logo on file.

export const JobBoardDemo = () => {
  const TOKEN = "live_6a1a28fd-6420-4492-aeb0-b297461d9de2";
  const SOCIAL_ICONS = {
    github: <svg aria-hidden="true" fill="currentColor" height="14" viewBox="0 0 16 16" width="14" xmlns="http://www.w3.org/2000/svg">
        <title>GitHub</title>
        <path d="M8 0a8 8 0 0 0-2.53 15.59c.4.07.55-.17.55-.38v-1.34c-2.23.48-2.7-1.07-2.7-1.07-.36-.93-.89-1.18-.89-1.18-.73-.5.05-.49.05-.49.8.06 1.23.83 1.23.83.72 1.23 1.87.87 2.33.67.07-.52.28-.87.5-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.83-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.22 2.2.82a7.6 7.6 0 0 1 4 0c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.52.56.83 1.28.83 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48v2.2c0 .21.15.46.55.38A8 8 0 0 0 8 0Z" />
      </svg>,
    linkedin: <svg aria-hidden="true" fill="currentColor" height="14" viewBox="0 0 16 16" width="14" xmlns="http://www.w3.org/2000/svg">
        <title>LinkedIn</title>
        <path d="M3.6 5.3H.8V14h2.8V5.3ZM2.2 1a1.6 1.6 0 1 0 0 3.2 1.6 1.6 0 0 0 0-3.2ZM15.2 14v-4.8c0-2.4-.5-4.1-3.3-4.1-1.3 0-2.2.7-2.6 1.4h-.04V5.3H6.5V14h2.8V9.7c0-1.1.2-2.2 1.6-2.2 1.4 0 1.5 1.3 1.5 2.3V14h2.8Z" />
      </svg>,
    twitter: <svg aria-hidden="true" fill="currentColor" height="14" viewBox="0 0 16 16" width="14" xmlns="http://www.w3.org/2000/svg">
        <title>X</title>
        <path d="M9.5 6.8 15.4 0h-1.4L8.9 5.9 4.8 0H0l6.2 9-6.2 7.2h1.4l5.4-6.3 4.3 6.3H16L9.5 6.8Zm-1.9 2.2-.6-.9L1.9 1h2.1l4 5.8.6.9 5.2 7.5h-2.1L7.6 9Z" />
      </svg>,
    youtube: <svg aria-hidden="true" fill="currentColor" height="14" viewBox="0 0 16 16" width="14" xmlns="http://www.w3.org/2000/svg">
        <title>YouTube</title>
        <path d="M15.7 4.3a2 2 0 0 0-1.4-1.4C13 2.5 8 2.5 8 2.5s-5 0-6.3.4A2 2 0 0 0 .3 4.3C0 5.6 0 8 0 8s0 2.4.3 3.7a2 2 0 0 0 1.4 1.4c1.3.4 6.3.4 6.3.4s5 0 6.3-.4a2 2 0 0 0 1.4-1.4C16 10.4 16 8 16 8s0-2.4-.3-3.7ZM6.4 10.4V5.6L10.5 8l-4.1 2.4Z" />
      </svg>,
    facebook: <svg aria-hidden="true" fill="currentColor" height="14" viewBox="0 0 16 16" width="14" xmlns="http://www.w3.org/2000/svg">
        <title>Facebook</title>
        <path d="M16 8a8 8 0 1 0-9.25 7.9v-5.59H4.72V8h2.03V6.24c0-2 1.2-3.11 3.02-3.11.88 0 1.79.16 1.79.16v1.97h-1.01c-1 0-1.31.62-1.31 1.25V8h2.22l-.35 2.31H9.25v5.59A8 8 0 0 0 16 8Z" />
      </svg>,
    instagram: <svg aria-hidden="true" fill="currentColor" height="14" viewBox="0 0 16 16" width="14" xmlns="http://www.w3.org/2000/svg">
        <title>Instagram</title>
        <path d="M8 1.44c2.14 0 2.39.01 3.23.05.78.03 1.2.17 1.49.28.37.14.64.32.92.6.28.28.46.55.6.92.11.29.25.71.28 1.49.04.84.05 1.09.05 3.23s-.01 2.39-.05 3.23c-.03.78-.17 1.2-.28 1.49-.14.37-.32.64-.6.92-.28.28-.55.46-.92.6-.29.11-.71.25-1.49.28-.84.04-1.09.05-3.23.05s-2.39-.01-3.23-.05c-.78-.03-1.2-.17-1.49-.28a2.48 2.48 0 0 1-.92-.6 2.48 2.48 0 0 1-.6-.92c-.11-.29-.25-.71-.28-1.49C1.45 10.39 1.44 10.14 1.44 8s.01-2.39.05-3.23c.03-.78.17-1.2.28-1.49.14-.37.32-.64.6-.92.28-.28.55-.46.92-.6.29-.11.71-.25 1.49-.28C5.61 1.45 5.86 1.44 8 1.44ZM8 0C5.83 0 5.55.01 4.7.05c-.85.04-1.43.17-1.94.37-.53.2-.98.48-1.42.93-.45.44-.72.89-.93 1.42-.2.51-.33 1.09-.37 1.94C.01 5.55 0 5.83 0 8s.01 2.45.05 3.3c.04.85.17 1.43.37 1.94.2.53.48.98.93 1.42.44.45.89.72 1.42.93.51.2 1.09.33 1.94.37C5.55 15.99 5.83 16 8 16s2.45-.01 3.3-.05c.85-.04 1.43-.17 1.94-.37.53-.2.98-.48 1.42-.93.45-.44.72-.89.93-1.42.2-.51.33-1.09.37-1.94.04-.85.05-1.13.05-3.3s-.01-2.45-.05-3.3c-.04-.85-.17-1.43-.37-1.94a3.92 3.92 0 0 0-.93-1.42 3.92 3.92 0 0 0-1.42-.93c-.51-.2-1.09-.33-1.94-.37C10.45.01 10.17 0 8 0Zm0 3.89a4.11 4.11 0 1 0 0 8.22 4.11 4.11 0 0 0 0-8.22Zm0 6.78a2.67 2.67 0 1 1 0-5.34 2.67 2.67 0 0 1 0 5.34Zm4.27-6.94a.96.96 0 1 0 0 1.92.96.96 0 0 0 0-1.92Z" />
      </svg>
  };
  const JOBS = [{
    role: "Senior Product Designer",
    company: "Figma",
    domain: "figma.com",
    location: "Remote",
    posted: "2h ago",
    description: "The collaborative design platform for building products: design, prototype, and gather feedback in one place.",
    socials: {
      facebook: "https://www.facebook.com/figmadesign",
      instagram: "https://www.instagram.com/figma",
      linkedin: "https://linkedin.com/company/figma",
      twitter: "https://twitter.com/figma",
      youtube: "https://www.youtube.com/@Figma"
    }
  }, {
    role: "Backend Engineer",
    company: "Stripe",
    domain: "stripe.com",
    location: "Dublin",
    posted: "5h ago",
    description: "A financial services platform to accept payments, build billing, and manage money movement.",
    socials: {
      github: "https://github.com/stripe",
      linkedin: "https://linkedin.com/company/stripe",
      twitter: "https://x.com/stripe",
      youtube: "https://www.youtube.com/@stripe"
    }
  }, {
    role: "Growth Marketer",
    company: "Notion",
    domain: "notion.so",
    location: "New York",
    posted: "1d ago",
    description: "The AI workspace where teams build agents, search across apps, and automate busywork.",
    socials: {
      linkedin: "https://linkedin.com/company/notionhq",
      twitter: "https://twitter.com/NotionHQ"
    }
  }, {
    role: "Operations Lead",
    company: "Airbnb",
    domain: "airbnb.com",
    location: "Dublin",
    posted: "2d ago",
    description: null,
    socials: {}
  }];
  const [openDomain, setOpenDomain] = useState(null);
  const openJob = JOBS.find(job => job.domain === openDomain) ?? null;
  const logoSrc = (domain, size) => `https://img.logo.dev/${domain}?token=${TOKEN}&size=${size}&retina=true&format=webp`;
  const toggle = domain => setOpenDomain(current => current === domain ? null : domain);
  return <div className="not-prose my-8 rounded-2xl border border-zinc-950/10 bg-gradient-to-br from-zinc-50 to-white p-6 shadow-sm sm:p-8 dark:border-white/10 dark:from-zinc-900 dark:to-zinc-950">
      <ul className="mx-auto max-w-md space-y-3">
        {JOBS.map(job => {
    const isOpen = job.domain === openDomain;
    return <li key={`${job.domain}-${job.role}`}>
              <button aria-expanded={isOpen} className={`flex w-full items-center gap-3 rounded-xl border bg-white p-4 text-left transition-[background-color,border-color] duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-zinc-950/20 motion-safe:active:scale-[0.99] dark:bg-zinc-900 dark:focus-visible:ring-white/20 ${isOpen ? "border-zinc-950/20 dark:border-white/20" : "border-zinc-950/10 hover:bg-zinc-50 dark:border-white/10 dark:hover:bg-zinc-800/50"}`} onClick={() => toggle(job.domain)} type="button">
                <img alt={`${job.company} logo`} className="h-10 w-10 rounded-lg object-contain" height="40" loading="lazy" src={logoSrc(job.domain, 40)} width="40" />
                <div className="min-w-0">
                  <div className="truncate text-sm font-semibold text-zinc-950 dark:text-white">
                    {job.role}
                  </div>
                  <div className="truncate text-xs text-zinc-500 dark:text-zinc-400">
                    {job.company} · {job.location}
                  </div>
                </div>
                <span className="ml-auto shrink-0 text-xs text-zinc-400">
                  {job.posted}
                </span>
              </button>
            </li>;
  })}
      </ul>

      {openJob && <div className="mx-auto mt-4 max-w-md rounded-xl border border-zinc-950/10 bg-white p-4 dark:border-white/10 dark:bg-zinc-900">
          <div className="flex items-center gap-3">
            <img alt={`${openJob.company} logo`} className="h-12 w-12 rounded-xl object-contain" height="48" key={openJob.domain} src={logoSrc(openJob.domain, 48)} width="48" />
            <div className="min-w-0">
              <div className="truncate text-base font-semibold text-zinc-950 dark:text-white">
                {openJob.company}
              </div>
              <div className="truncate text-xs text-zinc-400">
                {openJob.domain}
              </div>
            </div>
          </div>
          {openJob.description && <div className="mt-3 text-xs leading-relaxed text-zinc-600 dark:text-zinc-400">
              {openJob.description}
            </div>}
          {Object.keys(openJob.socials).length > 0 && <div className="mt-3 flex items-center gap-1 border-t border-zinc-950/5 pt-3 dark:border-white/5">
              {Object.entries(openJob.socials).filter(([network]) => SOCIAL_ICONS[network]).map(([network, url]) => <a aria-label={network} className="flex h-8 w-8 items-center justify-center rounded-lg text-zinc-500 transition-[background-color,color] duration-150 hover:bg-zinc-100 hover:text-zinc-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-zinc-950/20 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-white dark:focus-visible:ring-white/20" href={url} key={network} rel="noopener noreferrer" target="_blank">
                  {SOCIAL_ICONS[network]}
                </a>)}
            </div>}
        </div>}
    </div>;
};

Job boards, marketplaces, and directories render the same card thousands of times, and every card needs the employer's logo. At that volume, nobody can collect logos by hand.

A candidate scrolls the feed and every card shows its employer's logo, even listings from companies they've never heard of. Each logo is a [Logo API](/docs/logo-images/introduction) image URL built from the domain on the listing, so new employers get logos automatically.

## Demo

Here's the card rendering four sample listings:

<JobBoardDemo />

The listings are sample data, but every employer logo loads live from `img.logo.dev`. Select a listing to open a company card with the employer's description and social links.

## Build it with AI

<Prompt description="Want employer logos on your listing cards? This prompt gives your AI coding tool everything it needs to build it in your framework." actions={["copy", "cursor"]}>
  Build a job listing card with employer logos using Logo.dev.

  * Render a card showing the employer's logo, role title, company name, location, and posting age. The card repeats for every listing in a long feed.
  * Logos are plain image URLs built from the employer's domain: [https://img.logo.dev/:domain?token=LOGO\_DEV\_PUBLISHABLE\_KEY\&size=40\&retina=true\&format=webp](https://img.logo.dev/:domain?token=LOGO_DEV_PUBLISHABLE_KEY\&size=40\&retina=true\&format=webp)
  * Give the logo fixed 40px dimensions so cards keep their height while images load.
  * Domains without a logo return a generated monogram automatically, so the feed never shows a broken image.
  * Lazy-load the logos so infinite-scroll feeds only fetch what's visible.
  * Use a Logo.dev publishable key (safe in client code), never a secret key.
  * Reference implementation: [https://www.logo.dev/docs/use-cases/job-board-logos](https://www.logo.dev/docs/use-cases/job-board-logos)
</Prompt>

## Code

This example is in React, but you can get the same result in any frontend framework: the card is one image URL next to fields already on your listing. Paste `JobCard.tsx`, then render it as the Usage tab shows. The logo slot is a fixed 40px square, so cards keep their height while images load.

<CodeGroup>
  ```tsx JobCard.tsx theme={null}
  type Job = {
    role: string;
    company: string;
    domain: string;
    location: string;
    posted: string;
  };

  export function JobCard({ job, token }: { job: Job; token: string }) {
    return (
      <div className="flex items-center gap-3 rounded-xl border border-zinc-950/10 bg-white p-4">
        <img
          alt={`${job.company} logo`}
          className="h-10 w-10 rounded-lg object-contain"
          height={40}
          loading="lazy"
          src={`https://img.logo.dev/${job.domain}?token=${token}&size=40&retina=true&format=webp`}
          width={40}
        />
        <div className="min-w-0">
          <div className="truncate text-sm font-semibold">{job.role}</div>
          <div className="truncate text-xs text-zinc-500">
            {job.company} · {job.location}
          </div>
        </div>
        <span className="ml-auto shrink-0 text-xs text-zinc-400">
          {job.posted}
        </span>
      </div>
    );
  }
  ```

  ```tsx Usage theme={null}
  import { JobCard } from "./JobCard";

  export function Listings() {
    return (
      <JobCard
        job={{
          role: "Backend Engineer",
          company: "Stripe",
          domain: "stripe.com",
          location: "Dublin",
          posted: "5h ago",
        }}
        token="LOGO_DEV_PUBLISHABLE_KEY"
      />
    );
  }
  ```
</CodeGroup>

<Note>
  Your [publishable key](/docs/platform/api-keys) is built for client-side code, so
  you can ship it in the browser as-is.
</Note>

## How it works

* **Every employer logo is one image URL.** `img.logo.dev/:domain` turns the domain on the listing into a logo, and query parameters set the size and format. See all [image parameters](/docs/logo-images/introduction#parameters).
* **A feed of ten thousand employers never breaks.** When Logo.dev doesn't have a logo, it returns a generated monogram instead of a broken image, so there are no per-company checks. See [fallback images](/docs/logo-images/introduction#fallback-images).
* **The CDN absorbs the volume.** Logos are cached and served like any other image, so rendering the same card thousands of times costs normal image requests.
* **The card is plain markup.** Logos load as cards scroll into view and the image slot keeps a fixed size, so the feed never jumps. The component above carries the details.

## Make it your own

* **Reuse the card for any company list.** Swap the job fields for a one-line description and the same card serves VC portfolios and [directories](/docs/use-cases/integration-logos).
* **Resolve employers without domains.** Let posters pick their company with the [autocomplete](/docs/use-cases/company-autocomplete), or look up logos [by name](/docs/use-cases/logo-from-name).
* **Restyle the logos.** Add `&greyscale=true` for a uniform feed or `&theme=dark` for dark backgrounds. See all [image parameters](/docs/logo-images/introduction#parameters).

## Go further: build a company card

The domain that loads a logo can also pull a description and social profiles. Resolve the employer once by name, then show that brand data on a company card next to the listing.

You can turn a company name into full brand data with one server route. It calls the [Search API](/docs/brand-search/introduction) to resolve the name to a domain, then the [Brand API](/docs/brand/introduction) to read the description, brand colors, and social profiles. Both use your [secret key](/docs/platform/api-keys), and the Brand API is on every plan, metered in credits. The code runs on your server, so the browser only ever receives the finished result.

<CodeGroup>
  ```ts api/enrich-company.ts theme={null}
  const BASE = "https://api.logo.dev";
  const headers = { Authorization: `Bearer ${process.env.LOGO_DEV_SECRET_KEY}` };

  // Resolve a typed company name to a domain, then enrich it with brand data.
  export async function GET(request: Request) {
    const q = new URL(request.url).searchParams.get("q")?.trim();
    if (!q) {
      return Response.json(null);
    }

    // 1. Resolve the name to a domain with the Search API.
    const search = await fetch(`${BASE}/search?q=${encodeURIComponent(q)}`, {
      headers,
    });
    const [match] = search.ok ? await search.json() : [];
    if (!match) {
      return Response.json(null);
    }

    // 2. Enrich the domain with the Brand API.
    const res = await fetch(`${BASE}/brand/${match.domain}`, { headers });

    // Brand returns 202 while it indexes a domain for the first time. Pass that
    // signal through so the client can retry instead of caching empty brand data.
    if (res.status === 202) {
      return Response.json(
        { name: match.name, domain: match.domain, pending: true },
        { status: 202 }
      );
    }

    // A 401, 402, or 403 is an integration problem, not a sparse record. Pass the
    // status through so the client can act on it instead of showing empty fields.
    if (!(res.ok || res.status === 404)) {
      return Response.json({ error: "brand lookup failed" }, { status: res.status });
    }

    const brand = res.ok ? await res.json() : null;

    // A known domain can still come back sparse, so return the name and domain
    // either way and let the logo stand in until the rest fills in.
    return Response.json({
      name: match.name,
      domain: match.domain,
      description: brand?.description || null,
      colors: brand?.colors ?? [],
      socials: brand?.socials ?? {},
    });
  }
  ```
</CodeGroup>

The browser never sees your secret key. It calls `/api/enrich-company?q=...` and builds the logo URL from the returned domain with your [publishable key](/docs/platform/api-keys).

Every call to this route spends your account's Credits, so put it behind your own authentication and cache results by domain before you expose it to end users.

On the card, render the returned `description` and the social links in `socials`. Only the networks present in the response render, so an employer with two profiles shows two icons and one with five shows five. Employers with no brand data on file still show their logo and name. The [Brand API](/docs/brand/introduction) returns both fields, keyed off the domain the Search API resolved.

## Next steps

<CardGroup cols={2}>
  <Card title="Logo API" icon="image" href="/docs/logo-images/introduction">
    Browse every image URL parameter: size, format, theme, greyscale, and fallbacks.
  </Card>

  <Card title="Integration logos" icon="puzzle-piece" href="/docs/use-cases/integration-logos">
    Build the tile-based version for app marketplaces.
  </Card>
</CardGroup>
