Data Components

Generated Social

Draft a LinkedIn post or tweet anchored in what's actually happening in a space this week. Pick a format (LinkedIn long, LinkedIn short, Tweet, Twitter thread), enter an industry or topic, and Exa fetches the current zeitgeist before drafting a post that rides it.

Demo

Each draft includes a “Riding the wave” line naming the specific recent event the post is responding to, so you can verify the timeliness before publishing.

Long-form LinkedIn post riding a current debate in the space.

Try one of these:

How it works

One call to exa.search with type: "deep" per draft. The query is shaped per template: a single tweet looks at the last 7 days, a long-form LinkedIn post looks at the last 30. Exa pulls from Twitter/X, Hacker News, LinkedIn, trade press, and recent newsletters.

The model then writes a post that names a specific recent event, person, or release from the sources. The required zeitgeistAngle field forces the model to declare what current conversation the post is riding, which the renderer surfaces so the writer can sanity-check before posting.

Each template ships its own schema and systemPrompt in social-templates.ts. LinkedIn long-form returns a structured hook / body / cta so the renderer can highlight what shows above LinkedIn's “see more” cutoff. Twitter threads return an array of tweets with a 280-character ceiling per item.

The prompts explicitly ban the most overused content-creator clichés (“Here's the thing”, “Unpopular opinion:”, “Stop doing X. Start doing Y.”, “Agree?”) and require specificity from the sources over abstract claims. Sources are flattened from output.grounding, deduped by domain, and rendered as a favicon row below the post.

Source code

"use client";

import { useState } from "react";
import { CitationChip, safeDomain, type Citation } from "@/lib/citations";
import { TEMPLATES, type TemplateId } from "@/lib/social-templates";

type GenState =
  | { status: "idle" }
  | { status: "generating" }
  | {
      status: "done";
      template: TemplateId;
      post: GeneratedPost;
      citations: Citation[];
    }
  | { status: "error"; error: string };

type GeneratedPost = {
  zeitgeistAngle?: string;
  // linkedin-long
  hook?: string;
  paragraphs?: string[];
  kicker?: string;
  // tweet
  post?: string;
  // twitter-thread
  tweets?: string[];
};

export function GeneratedSocialExample() {
  const [template, setTemplate] = useState<TemplateId>("linkedin-long");
  const [topic, setTopic] = useState("");
  const [state, setState] = useState<GenState>({ status: "idle" });

  const active = TEMPLATES.find((t) => t.id === template)!;

  async function generate(tp: string, tpl: TemplateId) {
    const t = tp.trim();
    if (!t || state.status === "generating") return;
    setState({ status: "generating" });
    try {
      const res = await fetch("/api/generated-social", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ topic: t, template: tpl }),
      });
      const json = await res.json();
      if (!res.ok || json.error) {
        setState({
          status: "error",
          error: json.error || `Request failed (${res.status})`,
        });
        return;
      }
      setState({
        status: "done",
        template: tpl,
        post: json.post ?? {},
        citations: json.citations ?? [],
      });
    } catch (err) {
      setState({
        status: "error",
        error: err instanceof Error ? err.message : String(err),
      });
    }
  }

  const isGenerating = state.status === "generating";

  return (
    <div className="space-y-4">
      <div className="flex items-center gap-1 border-b border-[#e5e5e5] overflow-x-auto">
        {TEMPLATES.map((t) => {
          const isActive = t.id === template;
          return (
            <button
              key={t.id}
              type="button"
              onClick={() => {
                setTemplate(t.id);
                setTopic("");
                setState({ status: "idle" });
              }}
              className={`px-3 py-2 text-[13px] -mb-px border-b-2 whitespace-nowrap ${
                isActive
                  ? "border-[var(--color-brand)] text-[#111827] font-medium"
                  : "border-transparent text-[#6b7280] hover:text-[#111827]"
              }`}
            >
              {t.label}
            </button>
          );
        })}
      </div>

      <p className="text-[13px] text-[#6b7280]">{active.tagline}</p>

      <form
        onSubmit={(e) => {
          e.preventDefault();
          void generate(topic, template);
        }}
        className="border border-[#e5e5e5] bg-white overflow-hidden focus-within:border-[var(--color-brand)]"
      >
        <div className="flex items-center gap-2 px-3 py-2">
          <input
            type="text"
            value={topic}
            onChange={(e) => setTopic(e.target.value)}
            placeholder={active.placeholder}
            className="flex-1 text-[13px] text-[#111827] focus:outline-none"
          />
          <button
            type="submit"
            disabled={isGenerating || !topic.trim()}
            className="bg-[var(--color-brand)] text-white text-[13px] font-medium px-3.5 py-1.5 disabled:opacity-50"
          >
            {isGenerating ? "Drafting..." : "Draft"}
          </button>
        </div>
      </form>

      {state.status === "idle" && (
        <div className="text-[13px] text-[#6b7280]">
          <p className="mb-2">Try one of these:</p>
          <div className="flex flex-wrap gap-2">
            {active.examples.map((s) => (
              <button
                key={s}
                type="button"
                onClick={() => {
                  setTopic(s);
                  void generate(s, template);
                }}
                className="rounded-full border border-[#e5e5e5] bg-white px-3 py-1 text-[12px] text-[#374151] hover:border-[var(--color-brand)] hover:text-[#111827]"
              >
                {s}
              </button>
            ))}
          </div>
        </div>
      )}

      {state.status === "error" && (
        <div className="text-[13px] text-[#b91c1c]">{state.error}</div>
      )}

      {state.status === "generating" && (
        <div className="border border-[#e5e5e5] bg-white px-6 py-5">
          <PostSkeleton />
        </div>
      )}

      {state.status === "done" && (
        <PostResult
          template={state.template}
          post={state.post}
          citations={state.citations}
        />
      )}
    </div>
  );
}

function PostResult({
  template,
  post,
  citations,
}: {
  template: TemplateId;
  post: GeneratedPost;
  citations: Citation[];
}) {
  const uniqueCitations = dedupeByDomain(citations);
  return (
    <div className="space-y-3">
      {post.zeitgeistAngle && (
        <div className="border-l-2 border-[var(--color-brand)] pl-3 py-1">
          <div className="text-[10px] uppercase tracking-wider text-[#9ca3af] mb-0.5">
            Riding the wave
          </div>
          <p className="text-[12px] leading-[1.55] text-[#374151]">
            {asString(post.zeitgeistAngle)}
          </p>
        </div>
      )}

      {template === "linkedin-long" && <LinkedInLong post={post} />}
      {template === "linkedin-short" && <LinkedInShort post={post} />}
      {template === "tweet" && <TweetCard post={post} />}
      {template === "twitter-thread" && <TwitterThread post={post} />}

      {uniqueCitations.length > 0 && (
        <div className="flex items-center gap-2 flex-wrap pt-1">
          <span className="inline-flex items-center h-4 text-[11px] uppercase tracking-wider text-[#9ca3af] leading-none mr-1">
            Sources
          </span>
          <span className="inline-flex items-center gap-1">
            {uniqueCitations.map((c) => (
              <CitationChip key={c.id} citation={c} />
            ))}
          </span>
        </div>
      )}
    </div>
  );
}

function LinkedInLong({ post }: { post: GeneratedPost }) {
  const hook = asString(post.hook);
  const kicker = asString(post.kicker);
  const paragraphs = (post.paragraphs ?? [])
    .map(asString)
    .map((p) => p.trim())
    .filter(Boolean);
  return (
    <article className="border border-[#e5e5e5] bg-white">
      <LinkedInHeader />
      <div className="px-4 pb-3 text-[14px] leading-[1.55] text-[#111827] space-y-3">
        {hook && <p className="font-medium">{hook}</p>}
        {paragraphs.map((p, i) => (
          <p key={i}>{p}</p>
        ))}
        {kicker && <p className="font-medium">{kicker}</p>}
      </div>
      <LinkedInActions />
    </article>
  );
}

function LinkedInShort({ post }: { post: GeneratedPost }) {
  const paragraphs = (post.paragraphs ?? [])
    .map(asString)
    .map((p) => p.trim())
    .filter(Boolean);
  return (
    <article className="border border-[#e5e5e5] bg-white">
      <LinkedInHeader />
      <div className="px-4 pb-3 text-[14px] leading-[1.55] text-[#111827] space-y-3">
        {paragraphs.map((p, i) => (
          <p key={i}>{p}</p>
        ))}
      </div>
      <LinkedInActions />
    </article>
  );
}

function TweetCard({ post }: { post: GeneratedPost }) {
  const text = asString(post.post);
  const chars = text.length;
  return (
    <article className="border border-[#e5e5e5] bg-white px-4 py-3">
      <TwitterHeader />
      <p className="mt-2 text-[15px] leading-[1.4] text-[#0f1419] whitespace-pre-line">
        {text}
      </p>
      <div className="mt-3 flex items-center justify-between">
        <TwitterActions />
        <span
          className={`text-[11px] ${
            chars > 280 ? "text-[#dc2626]" : "text-[#9ca3af]"
          }`}
        >
          {chars}/280
        </span>
      </div>
    </article>
  );
}

function TwitterThread({ post }: { post: GeneratedPost }) {
  const tweets = (post.tweets ?? [])
    .map((t) => asString(t))
    .filter(Boolean);
  if (tweets.length === 0) return null;
  return (
    <div className="space-y-0">
      {tweets.map((t, i) => {
        const last = i === tweets.length - 1;
        const chars = t.length;
        return (
          <div key={i} className="flex gap-3">
            <div className="flex flex-col items-center pt-3">
              <div className="h-9 w-9 rounded-full bg-[#cfd9de] shrink-0" />
              {!last && <div className="w-px flex-1 bg-[#e5e5e5] my-1" />}
            </div>
            <article
              className={`flex-1 min-w-0 ${
                last ? "" : "border-b border-[#f0f0f0]"
              } py-3`}
            >
              <div className="flex items-baseline gap-2">
                <span className="font-medium text-[14px] text-[#0f1419]">
                  Your name
                </span>
                <span className="text-[13px] text-[#536471]">@you</span>
                <span className="text-[13px] text-[#536471]">·</span>
                <span className="text-[12px] text-[#536471]">
                  {i + 1}/{tweets.length}
                </span>
              </div>
              <p className="mt-1 text-[15px] leading-[1.4] text-[#0f1419] whitespace-pre-line">
                {t}
              </p>
              <div className="mt-1 text-[11px] text-[#9ca3af]">
                <span className={chars > 280 ? "text-[#dc2626]" : ""}>
                  {chars}/280
                </span>
              </div>
            </article>
          </div>
        );
      })}
    </div>
  );
}

function LinkedInHeader() {
  return (
    <div className="flex items-center gap-2 px-4 py-3">
      <div className="h-10 w-10 rounded-full bg-[#cfd9de] shrink-0" />
      <div className="min-w-0">
        <div className="text-[13px] font-medium text-[#0f1419] leading-tight">
          Your name
        </div>
        <div className="text-[11px] text-[#536471] leading-tight">
          Title at Company
        </div>
        <div className="text-[11px] text-[#536471] leading-tight">
          now &middot; Public
        </div>
      </div>
    </div>
  );
}

function LinkedInActions() {
  const items = ["Like", "Comment", "Repost", "Send"];
  return (
    <div className="border-t border-[#e5e5e5] px-3 py-1 flex items-center justify-around text-[12px] text-[#536471]">
      {items.map((it) => (
        <span key={it} className="px-2 py-1">
          {it}
        </span>
      ))}
    </div>
  );
}

function TwitterHeader() {
  return (
    <div className="flex items-center gap-2">
      <div className="h-9 w-9 rounded-full bg-[#cfd9de] shrink-0" />
      <div className="min-w-0">
        <div className="flex items-baseline gap-2">
          <span className="font-medium text-[14px] text-[#0f1419]">
            Your name
          </span>
          <span className="text-[13px] text-[#536471]">@you</span>
          <span className="text-[13px] text-[#536471]">·</span>
          <span className="text-[12px] text-[#536471]">now</span>
        </div>
      </div>
    </div>
  );
}

function TwitterActions() {
  const items = ["Reply", "Repost", "Like", "Bookmark"];
  return (
    <div className="flex items-center gap-4 text-[12px] text-[#536471]">
      {items.map((it) => (
        <span key={it}>{it}</span>
      ))}
    </div>
  );
}

function PostSkeleton() {
  return (
    <div className="space-y-2">
      <div className="flex items-center gap-2 mb-3">
        <div className="h-10 w-10 rounded-full bg-[#e5e7eb] animate-pulse" />
        <div className="space-y-1">
          <div className="h-3 w-32 rounded bg-[#e5e7eb] animate-pulse" />
          <div className="h-2 w-24 rounded bg-[#e5e7eb] animate-pulse" />
        </div>
      </div>
      <div className="h-3 w-3/4 rounded bg-[#e5e7eb] animate-pulse" />
      <div className="h-3 w-full rounded bg-[#e5e7eb] animate-pulse" />
      <div className="h-3 w-11/12 rounded bg-[#e5e7eb] animate-pulse" />
      <div className="h-3 w-2/3 rounded bg-[#e5e7eb] animate-pulse" />
    </div>
  );
}

function asString(v: unknown): string {
  if (typeof v === "string") return v;
  if (v == null) return "";
  if (typeof v === "object") {
    const o = v as Record<string, unknown>;
    if (typeof o.value === "string") return o.value;
    if (typeof o.text === "string") return o.text;
  }
  return String(v);
}

function dedupeByDomain(citations: Citation[]): Citation[] {
  const seen = new Set<string>();
  const unique: Citation[] = [];
  for (const c of citations) {
    const d = safeDomain(c.url);
    if (seen.has(d)) continue;
    seen.add(d);
    unique.push(c);
  }
  return unique;
}