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

# Build a company directory with logos

> Add recognizable company logos to directories, job boards, and account lists, with fallbacks and persistent corrections.

export const AgentSetupPrompt = ({children, variant = "setup"}) => {
  const isRecipe = variant === "recipe";
  const promptLabel = isRecipe ? "recipe prompt" : "setup prompt";
  const [copyState, setCopyState] = useState("idle");
  const contentRef = useRef(null);
  const detailsRef = useRef(null);
  const timerRef = useRef(null);
  const mountedRef = useRef(true);
  const pendingRef = useRef(false);
  useEffect(() => {
    mountedRef.current = true;
    return () => {
      mountedRef.current = false;
      if (timerRef.current !== null) clearTimeout(timerRef.current);
    };
  }, []);
  const copyPrompt = useCallback(async () => {
    if (pendingRef.current) return;
    pendingRef.current = true;
    if (timerRef.current !== null) {
      clearTimeout(timerRef.current);
      timerRef.current = null;
    }
    setCopyState("copying");
    try {
      const code = contentRef.current?.querySelector("pre code") ?? contentRef.current?.querySelector("pre");
      const prompt = code?.textContent;
      if (typeof prompt !== "string" || !prompt.trim()) {
        throw new Error("Prompt is not available.");
      }
      if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
        throw new Error("Clipboard access is not available.");
      }
      await navigator.clipboard.writeText(prompt);
      if (!mountedRef.current) return;
      setCopyState("copied");
      timerRef.current = setTimeout(() => {
        if (mountedRef.current) setCopyState("idle");
        timerRef.current = null;
      }, 4000);
    } catch {
      if (!mountedRef.current) return;
      if (detailsRef.current) detailsRef.current.open = true;
      setCopyState("error");
    } finally {
      pendingRef.current = false;
    }
  }, []);
  const feedback = copyState === "copied" ? `${isRecipe ? "Recipe" : "Setup"} prompt copied. Paste it into your coding agent.` : copyState === "error" ? "Couldn't copy automatically. The prompt is open below. Select and copy the text manually." : "";
  return <section className="agent-setup-prompt not-prose" aria-label={`Agent ${promptLabel}`}>
      <style>{`
        .agent-setup-prompt {
          --asp-surface: oklch(0.985 0.005 255);
          --asp-border: oklch(0.9 0.012 255);
          --asp-title: oklch(0.25 0.025 255);
          --asp-text: oklch(0.45 0.021 255);
          --asp-accent: oklch(0.49 0.2 264);
          --asp-accent-hover: oklch(0.44 0.19 264);
          --asp-on-accent: oklch(0.985 0.005 255);
          --asp-focus: oklch(0.6 0.19 255);
          margin: 1.5rem 0;
          border: 1px solid var(--asp-border);
          border-radius: 0.875rem;
          background: var(--asp-surface);
          color: var(--asp-title);
          font-family: inherit;
          min-width: 0;
        }
        .dark .agent-setup-prompt {
          --asp-surface: oklch(0.2 0.014 255);
          --asp-border: oklch(0.33 0.016 255);
          --asp-title: oklch(0.96 0.006 255);
          --asp-text: oklch(0.78 0.013 255);
          --asp-accent: oklch(0.73 0.15 255);
          --asp-accent-hover: oklch(0.8 0.11 255);
          --asp-on-accent: oklch(0.19 0.025 255);
          --asp-focus: oklch(0.78 0.14 255);
        }
        .agent-setup-prompt__header { padding: 1.125rem 1.25rem; }
        .agent-setup-prompt__row {
          display: flex;
          align-items: flex-start;
          justify-content: space-between;
          gap: 1.25rem;
        }
        .agent-setup-prompt__intro { min-width: 0; }
        .agent-setup-prompt__title {
          margin: 0;
          font-size: 1.125rem;
          font-weight: 650;
          line-height: 1.4;
          color: var(--asp-title);
        }
        .agent-setup-prompt__subtitle {
          max-width: 65ch;
          margin: 0.375rem 0 0;
          font-size: 0.875rem;
          line-height: 1.6;
          color: var(--asp-text);
        }
        .agent-setup-prompt__copy {
          display: inline-flex;
          flex-shrink: 0;
          align-items: center;
          justify-content: center;
          gap: 0.5rem;
          min-height: 2.75rem;
          padding: 0.625rem 0.875rem;
          border: 1px solid transparent;
          border-radius: 0.5rem;
          background: var(--asp-accent);
          color: var(--asp-on-accent);
          font: inherit;
          font-size: 0.8125rem;
          font-weight: 600;
          line-height: 1.4;
          cursor: pointer;
        }
        .agent-setup-prompt__copy:hover:not(:disabled) {
          background: var(--asp-accent-hover);
        }
        .agent-setup-prompt__copy:disabled { cursor: wait; }
        .agent-setup-prompt__copy:focus-visible,
        .agent-setup-prompt__summary:focus-visible {
          outline: 2px solid var(--asp-focus);
          outline-offset: 3px;
        }
        .agent-setup-prompt__feedback {
          margin: 0.75rem 0 0;
          font-size: 0.8125rem;
          line-height: 1.5;
          color: var(--asp-text);
        }
        .agent-setup-prompt__feedback:empty { margin: 0; }
        .agent-setup-prompt__details { border-top: 1px solid var(--asp-border); }
        .agent-setup-prompt__summary {
          display: flex;
          align-items: center;
          gap: 0.5rem;
          padding: 0.75rem 1.25rem;
          border-radius: 0 0 0.875rem 0.875rem;
          font-size: 0.8125rem;
          font-weight: 500;
          line-height: 1.5;
          color: var(--asp-text);
          list-style: none;
          cursor: pointer;
        }
        .agent-setup-prompt__summary::-webkit-details-marker { display: none; }
        .agent-setup-prompt__summary:hover { color: var(--asp-title); }
        .agent-setup-prompt__details[open] .agent-setup-prompt__chevron {
          transform: rotate(90deg);
        }
        .agent-setup-prompt__content { min-width: 0; padding: 0 1.25rem 1rem; }
        .agent-setup-prompt__content > :first-child { margin-top: 0; }
        .agent-setup-prompt__content > :last-child { margin-bottom: 0; }
        .agent-setup-prompt__content pre {
          max-height: 32rem;
          overflow: auto;
          white-space: pre-wrap;
          overflow-wrap: anywhere;
        }
        @media (max-width: 640px) {
          .agent-setup-prompt__header { padding: 1rem; }
          .agent-setup-prompt__row { flex-direction: column; gap: 0.875rem; }
          .agent-setup-prompt__copy { width: 100%; }
          .agent-setup-prompt__summary { padding: 0.75rem 1rem; }
          .agent-setup-prompt__content { padding: 0 1rem 1rem; }
        }
      `}</style>

      <div className="agent-setup-prompt__header">
        <div className="agent-setup-prompt__row">
          <div className="agent-setup-prompt__intro">
            <h2 className="agent-setup-prompt__title">
              {isRecipe ? "Have your agent implement this recipe" : "Set up Context.dev"}
            </h2>
            <p className="agent-setup-prompt__subtitle">
              {isRecipe ? "Copy this prompt into your coding agent to build the recipe in your project." : "Paste into your coding agent. It will ask about your goal and project before making changes."}
            </p>
          </div>
          <button className="agent-setup-prompt__copy" type="button" onClick={copyPrompt} disabled={copyState === "copying"} aria-busy={copyState === "copying"}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" aria-hidden="true" focusable="false">
              {copyState === "copied" ? <path d="m5 12 4 4L19 6" strokeLinecap="round" strokeLinejoin="round" /> : <>
                  <rect x="8" y="8" width="12" height="12" rx="2" />
                  <path d="M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3" />
                </>}
            </svg>
            {copyState === "copied" ? "Copied" : copyState === "copying" ? "Copying…" : `Copy ${promptLabel}`}
          </button>
        </div>
        <p className="agent-setup-prompt__feedback" role="status" aria-live="polite" aria-atomic="true">
          {feedback}
        </p>
      </div>

      <details className="agent-setup-prompt__details" ref={detailsRef}>
        <summary className="agent-setup-prompt__summary">
          <svg className="agent-setup-prompt__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" aria-hidden="true" focusable="false">
            <path d="m9 5 7 7-7 7" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
          {`View ${promptLabel}`}
        </summary>
        <div className="agent-setup-prompt__content" ref={contentRef}>
          {children}
        </div>
      </details>
    </section>;
};

<AgentSetupPrompt variant="recipe">
  ```text Recipe prompt theme={null}
  Implement this recipe in my project:
  https://docs.context.dev/use-cases/company-directory-logos

  Read the recipe and linked API guides, inspect this project's stack, and build the complete flow using its existing conventions.

  Build a company directory with direct Logo Link image embeds using a public client ID and allowed referring domains. Support icon or wordmark choices, light and dark backgrounds, missing-image fallbacks, and persistent domain corrections or logo removal. Keep names readable when logos fail. Do not download, persist, or rehost Logo Link images; use server-side Brand retrieval only if richer profiles are needed.

  Reuse existing Logo Link configuration. Add focused tests, run the relevant checks, and document the public client ID, allowed referring domains, and how to try the directory.
  ```
</AgentSetupPrompt>

Start with company names and verified domains, then add a logo to each row. Keep the directory useful when an image is missing, a company changes its name, or the first match belongs to a parent company.

[Vizzy uses company branding in profiles and job boards](https://www.context.dev/blog/vizzy-beautifies-profiles-and-job-boards-with-brand-dev). This recipe applies that pattern to a React directory.

## Choose the logo source

| Your application needs                               | Use                                                                |
| ---------------------------------------------------- | ------------------------------------------------------------------ |
| One hosted image for direct display                  | [Logo Link](/guides/get-logo-from-url)                             |
| Multiple logo variants, colors, or a company profile | [Brand retrieval](/guides/get-brand-data)                          |
| Assets for a document or creative export             | Brand retrieval, with your own asset review and rendering workflow |

Logo Link uses a public client ID. Configure allowed referring domains in the [Logo Link dashboard](https://context.dev/dashboard/logolink), including the domains used for development and production. Keep secret API keys on the server when using Brand retrieval.

Embed Logo Link images directly. Do not download, persist, or rehost the returned image files. Normal browser and CDN caching directed by the response headers is allowed.

## Keep identity separate from presentation

Use your own company ID as the record key. A domain is an input to logo lookup; it should not replace the identity of an existing directory entry.

```typescript Directory model theme={null}
type Company = {
  id: string;
  name: string;
  domain: string | null;
  // Saved separately from automatically refreshed company data.
  domainOverride: string | null;
  hideLogo: boolean;
};

function logoUrl(
  domain: string,
  publicClientId: string,
  theme: "light" | "dark",
  type: "icon" | "wordmark",
) {
  const params = new URLSearchParams({ publicClientId, domain, theme, type });
  return `https://logos.context.dev/?${params}`;
}
```

Validate domains when importing or editing a company: accept a hostname such as `stripe.com`, and keep paths, email addresses, and unrelated platform domains out of the lookup field. Confirm the company when several entities share a name.

## Render a resilient directory

This component receives persisted company records and an edit callback from your application. Copy `Company` and `logoUrl` above into the same module. The component key resets a failed image when the domain or display settings change.

```tsx CompanyDirectory.tsx theme={null}
"use client";

import { useState } from "react";

function CompanyLogo({
  company, src, wordmark,
}: { company: Company; src: string | null; wordmark: boolean }) {
  const [failed, setFailed] = useState(false);
  const initials = company.name.trim().slice(0, 2).toUpperCase() || "?";
  const width = wordmark ? 112 : 40;

  return (
    <span style={{ width, height: 40, display: "grid", placeItems: "center" }}>
      {src && !failed ? (
        <img
          src={src}
          alt={`${company.name} logo`}
          width={width}
          height={40}
          loading="lazy"
          decoding="async"
          style={{ objectFit: "contain", maxWidth: "100%" }}
          onError={() => setFailed(true)}
        />
      ) : (
        <span aria-hidden="true">{initials}</span>
      )}
    </span>
  );
}

export function CompanyDirectory({
  companies, publicClientId, onEdit,
}: {
  companies: Company[];
  publicClientId: string;
  onEdit: (company: Company) => void;
}) {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  const [type, setType] = useState<"icon" | "wordmark">("icon");

  return (
    <section style={{
      background: theme === "dark" ? "#171717" : "#ffffff",
      color: theme === "dark" ? "#fafafa" : "#171717",
      padding: 24,
    }}>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        Use {theme === "light" ? "dark" : "light"} background
      </button>
      <button onClick={() => setType(type === "icon" ? "wordmark" : "icon")}>
        Show {type === "icon" ? "wordmarks" : "icons"}
      </button>
      <ul style={{ listStyle: "none", padding: 0 }}>
        {companies.map((company) => {
          const domain = company.domainOverride ?? company.domain;
          const src = domain && !company.hideLogo
            ? logoUrl(domain, publicClientId, theme, type)
            : null;
          return (
            <li key={company.id} style={{
              display: "flex", alignItems: "center", gap: 16, padding: "12px 0",
              flexWrap: "wrap",
            }}>
              <CompanyLogo
                key={src ?? "fallback"}
                company={company}
                src={src}
                wordmark={type === "wordmark"}
              />
              <span>{company.name}</span>
              <button onClick={() => onEdit(company)}
                aria-label={`Edit brand for ${company.name}`}>
                Edit brand
              </button>
            </li>
          );
        })}
      </ul>
    </section>
  );
}
```

`theme` describes the background where the image will appear: `light` prefers a dark asset, and `dark` prefers a light asset. It does not recolor the company's logo. Keep the company name visible even when every image is blocked.

## Save corrections independently

Connect **Edit brand** to a form that can change the lookup domain or hide the logo. Save those decisions against the company ID in your database, with the editor and update time. Reload the saved record after a successful edit.

On automatic enrichment, update `domain` but leave `domainOverride` and `hideLogo` untouched. Provide a separate **Use automatic branding** action that clears the override. This lets a correction survive a refresh without preventing future automatic updates.

| Situation                       | Directory behavior                                       |
| ------------------------------- | -------------------------------------------------------- |
| No domain or no available image | Show the name and initials.                              |
| Parent-company or platform logo | Let an editor correct the domain or hide the image.      |
| Image does not fit the surface  | Switch the theme or choose a reviewed Brand variant.     |
| Temporary image failure         | Keep the row usable; retry on a later visit.             |
| Rebrand                         | Refresh automatic data and retain explicit user choices. |

A logo does not establish affiliation, endorsement, or legal identity. Keep any such claims in separately verified directory fields.

## Check the result

Try the directory with a known domain, no domain, and a deliberately unavailable image. Switch between light and dark backgrounds and between icons and wordmarks. Save a corrected domain, reload the page, then run enrichment again: the correction should remain.

<CardGroup cols={2}>
  <Card title="Logo Link setup" icon="image" href="/guides/get-logo-from-url">
    Configure the public client ID, referring domains, and image parameters.
  </Card>

  <Card title="Branded documents" icon="file-lines" href="/use-cases/branded-documents">
    Use reviewed brand assets in reports and proposals.
  </Card>
</CardGroup>
