Просмотр исходного кода

enterprise: add social card meta tags to share pages

- Add og:image and twitter:image meta tags for better social sharing
- Generate dynamic social card URLs with session title, models, and version
- Include description meta tag for search engines
Dax Raad 2 месяцев назад
Родитель
Сommit
85c01e8694
3 измененных файлов с 246 добавлено и 209 удалено
  1. 1 0
      bun.lock
  2. 1 0
      packages/enterprise/package.json
  3. 244 209
      packages/enterprise/src/routes/share/[shareID].tsx

+ 1 - 0
bun.lock

@@ -179,6 +179,7 @@
         "aws4fetch": "^1.0.20",
         "hono": "catalog:",
         "hono-openapi": "catalog:",
+        "js-base64": "3.7.7",
         "luxon": "catalog:",
         "nitro": "3.0.1-alpha.1",
         "solid-js": "catalog:",

+ 1 - 0
packages/enterprise/package.json

@@ -20,6 +20,7 @@
     "@solidjs/meta": "catalog:",
     "hono": "catalog:",
     "hono-openapi": "catalog:",
+    "js-base64": "3.7.7",
     "luxon": "catalog:",
     "nitro": "3.0.1-alpha.1",
     "solid-js": "catalog:",

+ 244 - 209
packages/enterprise/src/routes/share/[shareID].tsx

@@ -24,6 +24,7 @@ import { Diff as SSRDiff } from "@opencode-ai/ui/diff-ssr"
 import { clientOnly } from "@solidjs/start"
 import { type IconName } from "@opencode-ai/ui/icons/provider"
 import { Meta } from "@solidjs/meta"
+import { Base64 } from "js-base64"
 
 const ClientOnlyDiff = clientOnly(() => import("@opencode-ai/ui/diff").then((m) => ({ default: m.Diff })))
 
@@ -42,6 +43,7 @@ const getData = query(async (shareID) => {
   const data = await Share.data(shareID)
   const result: {
     sessionID: string
+    shareID: string
     session: Session[]
     session_diff: {
       [sessionID: string]: FileDiff[]
@@ -66,6 +68,7 @@ const getData = query(async (shareID) => {
     }
   } = {
     sessionID: share.sessionID,
+    shareID,
     session: [],
     session_diff: {
       [share.sessionID]: [],
@@ -160,239 +163,271 @@ export default function () {
           const match = createMemo(() => Binary.search(data().session, data().sessionID, (s) => s.id))
           if (!match().found) throw new Error(`Session ${data().sessionID} not found`)
           const info = createMemo(() => data().session[match().index])
+          const ogImage = createMemo(() => {
+            const models = new Set<string>()
+            const messages = data().message[data().sessionID] ?? []
+            for (const msg of messages) {
+              if (msg.role === "assistant" && msg.modelID) {
+                models.add(msg.modelID)
+              }
+            }
+            const modelIDs = Array.from(models)
+            const encodedTitle = encodeURIComponent(Base64.encode(encodeURIComponent(info().title.substring(0, 700))))
+            let modelParam: string
+            if (modelIDs.length === 1) {
+              modelParam = modelIDs[0]
+            } else if (modelIDs.length === 2) {
+              modelParam = encodeURIComponent(`${modelIDs[0]} & ${modelIDs[1]}`)
+            } else if (modelIDs.length > 2) {
+              modelParam = encodeURIComponent(`${modelIDs[0]} & ${modelIDs.length - 1} others`)
+            } else {
+              modelParam = "unknown"
+            }
+            const version = `v${info().version}`
+            return `https://social-cards.sst.dev/opencode-share/${encodedTitle}.png?model=${modelParam}&version=${version}&id=${data().shareID}`
+          })
 
           return (
-            <DiffComponentProvider component={ClientOnlyDiff}>
-              <DataProvider data={data()} directory={info().directory}>
-                {iife(() => {
-                  const [store, setStore] = createStore({
-                    messageId: undefined as string | undefined,
-                  })
-                  const messages = createMemo(() =>
-                    data().sessionID
-                      ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
-                          (a, b) => b.time.created - a.time.created,
-                        )
-                      : [],
-                  )
-                  const firstUserMessage = createMemo(() => messages().at(0))
-                  const activeMessage = createMemo(
-                    () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
-                  )
-                  function setActiveMessage(message: UserMessage | undefined) {
-                    if (message) {
-                      setStore("messageId", message.id)
-                    } else {
-                      setStore("messageId", undefined)
+            <>
+              <Meta name="description" content="opencode - The AI coding agent built for the terminal." />
+              <Meta property="og:image" content={ogImage()} />
+              <Meta name="twitter:image" content={ogImage()} />
+              <DiffComponentProvider component={ClientOnlyDiff}>
+                <DataProvider data={data()} directory={info().directory}>
+                  {iife(() => {
+                    const [store, setStore] = createStore({
+                      messageId: undefined as string | undefined,
+                    })
+                    const messages = createMemo(() =>
+                      data().sessionID
+                        ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
+                            (a, b) => b.time.created - a.time.created,
+                          )
+                        : [],
+                    )
+                    const firstUserMessage = createMemo(() => messages().at(0))
+                    const activeMessage = createMemo(
+                      () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
+                    )
+                    function setActiveMessage(message: UserMessage | undefined) {
+                      if (message) {
+                        setStore("messageId", message.id)
+                      } else {
+                        setStore("messageId", undefined)
+                      }
                     }
-                  }
-                  const provider = createMemo(() => activeMessage()?.model?.providerID)
-                  const modelID = createMemo(() => activeMessage()?.model?.modelID)
-                  const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
-                  const diffs = createMemo(() => {
-                    const diffs = data().session_diff[data().sessionID] ?? []
-                    const preloaded = data().session_diff_preload[data().sessionID] ?? []
-                    return diffs.map((diff) => ({
-                      ...diff,
-                      preloaded: preloaded.find((d) => d.newFile.name === diff.file),
-                    }))
-                  })
-                  const splitDiffs = createMemo(() => {
-                    const diffs = data().session_diff[data().sessionID] ?? []
-                    const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
-                    return diffs.map((diff) => ({
-                      ...diff,
-                      preloaded: preloaded.find((d) => d.newFile.name === diff.file),
-                    }))
-                  })
+                    const provider = createMemo(() => activeMessage()?.model?.providerID)
+                    const modelID = createMemo(() => activeMessage()?.model?.modelID)
+                    const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
+                    const diffs = createMemo(() => {
+                      const diffs = data().session_diff[data().sessionID] ?? []
+                      const preloaded = data().session_diff_preload[data().sessionID] ?? []
+                      return diffs.map((diff) => ({
+                        ...diff,
+                        preloaded: preloaded.find((d) => d.newFile.name === diff.file),
+                      }))
+                    })
+                    const splitDiffs = createMemo(() => {
+                      const diffs = data().session_diff[data().sessionID] ?? []
+                      const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
+                      return diffs.map((diff) => ({
+                        ...diff,
+                        preloaded: preloaded.find((d) => d.newFile.name === diff.file),
+                      }))
+                    })
 
-                  const title = () => (
-                    <div class="flex flex-col gap-4">
-                      <div class="h-8 flex gap-4 items-center justify-start self-stretch">
-                        <div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
-                          <Mark class="shrink-0 w-3 my-0.5" />
-                          <div class="text-12-mono text-text-base">v{info().version}</div>
-                        </div>
-                        <div class="flex gap-2 items-center">
-                          <ProviderIcon id={provider() as IconName} class="size-3.5 shrink-0 text-icon-strong-base" />
-                          <div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
-                        </div>
-                        <div class="text-12-regular text-text-weaker">
-                          {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
+                    const title = () => (
+                      <div class="flex flex-col gap-4">
+                        <div class="h-8 flex gap-4 items-center justify-start self-stretch">
+                          <div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
+                            <Mark class="shrink-0 w-3 my-0.5" />
+                            <div class="text-12-mono text-text-base">v{info().version}</div>
+                          </div>
+                          <div class="flex gap-2 items-center">
+                            <ProviderIcon id={provider() as IconName} class="size-3.5 shrink-0 text-icon-strong-base" />
+                            <div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
+                          </div>
+                          <div class="text-12-regular text-text-weaker">
+                            {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
+                          </div>
                         </div>
+                        <div class="text-left text-16-medium text-text-strong">{info().title}</div>
                       </div>
-                      <div class="text-left text-16-medium text-text-strong">{info().title}</div>
-                    </div>
-                  )
+                    )
 
-                  const turns = () => (
-                    <div class="relative mt-2 pt-6 pb-8 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
-                      <div class="px-4">{title()}</div>
-                      <div class="flex flex-col gap-15 items-start justify-start mt-4">
-                        <For each={messages()}>
-                          {(message) => (
-                            <SessionTurn
-                              sessionID={data().sessionID}
-                              messageID={message.id}
-                              classes={{
-                                root: "min-w-0 w-full relative",
-                                content:
-                                  "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
-                                container: "px-4",
-                              }}
-                            />
-                          )}
-                        </For>
-                      </div>
-                      <div class="px-4 flex items-center justify-center pt-20 pb-8 shrink-0">
-                        <Logo class="w-58.5 opacity-12" />
+                    const turns = () => (
+                      <div class="relative mt-2 pt-6 pb-8 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
+                        <div class="px-4">{title()}</div>
+                        <div class="flex flex-col gap-15 items-start justify-start mt-4">
+                          <For each={messages()}>
+                            {(message) => (
+                              <SessionTurn
+                                sessionID={data().sessionID}
+                                messageID={message.id}
+                                classes={{
+                                  root: "min-w-0 w-full relative",
+                                  content:
+                                    "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
+                                  container: "px-4",
+                                }}
+                              />
+                            )}
+                          </For>
+                        </div>
+                        <div class="px-4 flex items-center justify-center pt-20 pb-8 shrink-0">
+                          <Logo class="w-58.5 opacity-12" />
+                        </div>
                       </div>
-                    </div>
-                  )
+                    )
 
-                  const wide = createMemo(() => diffs().length === 0)
+                    const wide = createMemo(() => diffs().length === 0)
 
-                  return (
-                    <div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
-                      <header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
-                        <div class="">
-                          <a href="https://opencode.ai">
-                            <Mark />
-                          </a>
-                        </div>
-                        <div class="flex gap-3 items-center">
-                          <IconButton
-                            as={"a"}
-                            href="https://github.com/sst/opencode"
-                            target="_blank"
-                            icon="github"
-                            variant="ghost"
-                          />
-                          <IconButton
-                            as={"a"}
-                            href="https://opencode.ai/discord"
-                            target="_blank"
-                            icon="discord"
-                            variant="ghost"
-                          />
-                        </div>
-                      </header>
-                      <div class="select-text flex flex-col flex-1 min-h-0">
-                        <div
-                          classList={{ "hidden w-full flex-1 min-h-0": true, "md:flex": wide(), "lg:flex": !wide() }}
-                        >
+                    return (
+                      <div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
+                        <header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
+                          <div class="">
+                            <a href="https://opencode.ai">
+                              <Mark />
+                            </a>
+                          </div>
+                          <div class="flex gap-3 items-center">
+                            <IconButton
+                              as={"a"}
+                              href="https://github.com/sst/opencode"
+                              target="_blank"
+                              icon="github"
+                              variant="ghost"
+                            />
+                            <IconButton
+                              as={"a"}
+                              href="https://opencode.ai/discord"
+                              target="_blank"
+                              icon="discord"
+                              variant="ghost"
+                            />
+                          </div>
+                        </header>
+                        <div class="select-text flex flex-col flex-1 min-h-0">
                           <div
-                            classList={{
-                              "@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true,
-                              "mx-auto max-w-146": !wide(),
-                            }}
+                            classList={{ "hidden w-full flex-1 min-h-0": true, "md:flex": wide(), "lg:flex": !wide() }}
                           >
                             <div
                               classList={{
-                                "w-full flex justify-start items-start min-w-0": true,
-                                "max-w-146 mx-auto px-6": wide(),
-                                "pr-6 pl-18": !wide() && messages().length > 1,
-                                "px-6": !wide() && messages().length === 1,
+                                "@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true,
+                                "mx-auto max-w-146": !wide(),
                               }}
                             >
-                              {title()}
-                            </div>
-                            <div class="flex items-start justify-start h-full min-h-0">
-                              <SessionMessageRail
-                                messages={messages()}
-                                current={activeMessage()}
-                                onMessageSelect={setActiveMessage}
-                                wide={wide()}
-                              />
-                              <SessionTurn
-                                sessionID={data().sessionID}
-                                messageID={store.messageId ?? firstUserMessage()!.id!}
-                                classes={{
-                                  root: "grow",
-                                  content: "flex flex-col justify-between items-start",
-                                  container:
-                                    "w-full pb-20 " +
-                                    (wide() ? "max-w-146 mx-auto px-6" : messages().length > 1 ? "pr-6 pl-18" : "px-6"),
+                              <div
+                                classList={{
+                                  "w-full flex justify-start items-start min-w-0": true,
+                                  "max-w-146 mx-auto px-6": wide(),
+                                  "pr-6 pl-18": !wide() && messages().length > 1,
+                                  "px-6": !wide() && messages().length === 1,
                                 }}
                               >
-                                <div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
-                                  <Logo class="w-58.5 opacity-12" />
-                                </div>
-                              </SessionTurn>
-                            </div>
-                          </div>
-                          <Show when={diffs().length > 0}>
-                            <DiffComponentProvider component={SSRDiff}>
-                              <div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
-                                <SessionReview
-                                  class="@4xl:hidden"
-                                  diffs={diffs()}
-                                  classes={{
-                                    root: "pb-20",
-                                    header: "px-6",
-                                    container: "px-6",
-                                  }}
+                                {title()}
+                              </div>
+                              <div class="flex items-start justify-start h-full min-h-0">
+                                <SessionMessageRail
+                                  messages={messages()}
+                                  current={activeMessage()}
+                                  onMessageSelect={setActiveMessage}
+                                  wide={wide()}
                                 />
-                                <SessionReview
-                                  split
-                                  class="hidden @4xl:flex"
-                                  diffs={splitDiffs()}
+                                <SessionTurn
+                                  sessionID={data().sessionID}
+                                  messageID={store.messageId ?? firstUserMessage()!.id!}
                                   classes={{
-                                    root: "pb-20",
-                                    header: "px-6",
-                                    container: "px-6",
+                                    root: "grow",
+                                    content: "flex flex-col justify-between items-start",
+                                    container:
+                                      "w-full pb-20 " +
+                                      (wide()
+                                        ? "max-w-146 mx-auto px-6"
+                                        : messages().length > 1
+                                          ? "pr-6 pl-18"
+                                          : "px-6"),
                                   }}
-                                />
+                                >
+                                  <div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
+                                    <Logo class="w-58.5 opacity-12" />
+                                  </div>
+                                </SessionTurn>
                               </div>
-                            </DiffComponentProvider>
-                          </Show>
-                        </div>
-                        <Switch>
-                          <Match when={diffs().length > 0}>
-                            <Tabs classList={{ "md:hidden": wide(), "lg:hidden": !wide() }}>
-                              <Tabs.List>
-                                <Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
-                                  Session
-                                </Tabs.Trigger>
-                                <Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
-                                  {diffs().length} Files Changed
-                                </Tabs.Trigger>
-                              </Tabs.List>
-                              <Tabs.Content value="session" class="!overflow-hidden">
-                                {turns()}
-                              </Tabs.Content>
-                              <Tabs.Content
-                                forceMount
-                                value="review"
-                                class="!overflow-hidden hidden data-[selected]:block"
-                              >
-                                <div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
-                                  <DiffComponentProvider component={SSRDiff}>
-                                    <SessionReview
-                                      diffs={diffs()}
-                                      classes={{
-                                        root: "pb-20",
-                                        header: "px-4",
-                                        container: "px-4",
-                                      }}
-                                    />
-                                  </DiffComponentProvider>
-                                </div>
-                              </Tabs.Content>
-                            </Tabs>
-                          </Match>
-                          <Match when={true}>
-                            <div classList={{ "!overflow-hidden": true, "md:hidden": wide(), "lg:hidden": !wide() }}>
-                              {turns()}
                             </div>
-                          </Match>
-                        </Switch>
+                            <Show when={diffs().length > 0}>
+                              <DiffComponentProvider component={SSRDiff}>
+                                <div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
+                                  <SessionReview
+                                    class="@4xl:hidden"
+                                    diffs={diffs()}
+                                    classes={{
+                                      root: "pb-20",
+                                      header: "px-6",
+                                      container: "px-6",
+                                    }}
+                                  />
+                                  <SessionReview
+                                    split
+                                    class="hidden @4xl:flex"
+                                    diffs={splitDiffs()}
+                                    classes={{
+                                      root: "pb-20",
+                                      header: "px-6",
+                                      container: "px-6",
+                                    }}
+                                  />
+                                </div>
+                              </DiffComponentProvider>
+                            </Show>
+                          </div>
+                          <Switch>
+                            <Match when={diffs().length > 0}>
+                              <Tabs classList={{ "md:hidden": wide(), "lg:hidden": !wide() }}>
+                                <Tabs.List>
+                                  <Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
+                                    Session
+                                  </Tabs.Trigger>
+                                  <Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
+                                    {diffs().length} Files Changed
+                                  </Tabs.Trigger>
+                                </Tabs.List>
+                                <Tabs.Content value="session" class="!overflow-hidden">
+                                  {turns()}
+                                </Tabs.Content>
+                                <Tabs.Content
+                                  forceMount
+                                  value="review"
+                                  class="!overflow-hidden hidden data-[selected]:block"
+                                >
+                                  <div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
+                                    <DiffComponentProvider component={SSRDiff}>
+                                      <SessionReview
+                                        diffs={diffs()}
+                                        classes={{
+                                          root: "pb-20",
+                                          header: "px-4",
+                                          container: "px-4",
+                                        }}
+                                      />
+                                    </DiffComponentProvider>
+                                  </div>
+                                </Tabs.Content>
+                              </Tabs>
+                            </Match>
+                            <Match when={true}>
+                              <div classList={{ "!overflow-hidden": true, "md:hidden": wide(), "lg:hidden": !wide() }}>
+                                {turns()}
+                              </div>
+                            </Match>
+                          </Switch>
+                        </div>
                       </div>
-                    </div>
-                  )
-                })}
-              </DataProvider>
-            </DiffComponentProvider>
+                    )
+                  })}
+                </DataProvider>
+              </DiffComponentProvider>
+            </>
           )
         }}
       </Show>