Skip to content

Commit 446cc82

Browse files
committed
Add SEO and GEO autoresearch loop
1 parent fd01d01 commit 446cc82

92 files changed

Lines changed: 14906 additions & 100 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build-js.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ jobs:
3333
- name: Run Packages CI
3434
run: pnpm --filter "./packages/**" --workspace-concurrency=1 run ci
3535

36+
- name: Verify autoresearch control plane
37+
run: |
38+
pnpm --filter @openuidev/autoresearch typecheck
39+
pnpm --filter @openuidev/autoresearch test
40+
pnpm --filter @openuidev/autoresearch build
41+
42+
- name: Verify docs SEO policy and types
43+
run: |
44+
pnpm --filter @openuidev/docs test:seo
45+
pnpm --filter @openuidev/docs types:check
46+
3647
build-examples:
3748
runs-on: ubuntu-latest
3849
steps:

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,6 @@ draft/
5858

5959
# Local raw-asset drop folder (copied into docs/public to serve)
6060
/assets/
61+
62+
# Local autoresearch outputs may contain production analytics.
63+
tools/autoresearch/runs/

AUTORESEARCH_IMPLEMENTATION.md

Lines changed: 533 additions & 0 deletions
Large diffs are not rendered by default.

docs/app/(home)/components/Button/Button.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
import { copyText } from "@/lib/copy-text";
44
import { Check, Copy } from "lucide-react";
55
import Link from "next/link";
6-
import { useEffect, useRef, useState, type ButtonHTMLAttributes, type ReactNode } from "react";
6+
import {
7+
useEffect,
8+
useRef,
9+
useState,
10+
type ButtonHTMLAttributes,
11+
type MouseEventHandler,
12+
type ReactNode,
13+
} from "react";
714
import styles from "./Button.module.css";
815

916
type ButtonType = ButtonHTMLAttributes<HTMLButtonElement>["type"];
@@ -133,6 +140,7 @@ interface PillLinkProps {
133140
external?: boolean;
134141
variant?: PillVariant;
135142
fullWidthMobile?: boolean;
143+
onClick?: MouseEventHandler<HTMLAnchorElement>;
136144
}
137145

138146
const VARIANT_CLASS_MAP: Record<PillVariant, string> = {
@@ -149,6 +157,7 @@ export function PillLink({
149157
external = false,
150158
variant,
151159
fullWidthMobile = false,
160+
onClick,
152161
}: PillLinkProps) {
153162
const composedClassName = [
154163
variant ? styles.pillBase : "",
@@ -168,14 +177,20 @@ export function PillLink({
168177

169178
if (external) {
170179
return (
171-
<a href={href} target="_blank" rel="noopener noreferrer" className={composedClassName}>
180+
<a
181+
href={href}
182+
target="_blank"
183+
rel="noopener noreferrer"
184+
className={composedClassName}
185+
onClick={onClick}
186+
>
172187
{content}
173188
</a>
174189
);
175190
}
176191

177192
return (
178-
<Link href={href} className={composedClassName}>
193+
<Link href={href} className={composedClassName} onClick={onClick}>
179194
{content}
180195
</Link>
181196
);

docs/app/(home)/components/GitHubButton/GitHubButton.tsx

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"use client";
22

3-
import type { JSX, ReactNode } from "react";
43
import { GitHubIcon, useGitHubStarCount } from "@/components/brand-logo";
4+
import { captureWebsiteEvent, getGitHubRepository } from "@/lib/analytics/client";
5+
import type { JSX, ReactNode } from "react";
56
import { PillLink } from "../Button/Button";
67
import styles from "./GitHubButton.module.css";
78

@@ -79,6 +80,20 @@ function cx(...classNames: Array<string | undefined>): string {
7980
return classNames.filter(Boolean).join(" ").trim();
8081
}
8182

83+
function captureGitHubCta(
84+
href: string,
85+
placement:
86+
| "github_button_desktop_glow"
87+
| "github_button_desktop_pill"
88+
| "github_button_mobile_banner",
89+
) {
90+
captureWebsiteEvent("github_cta_clicked", {
91+
destination_url: href,
92+
placement,
93+
repository: getGitHubRepository(href),
94+
});
95+
}
96+
8297
export function GitHubButton({
8398
href = DEFAULT_GITHUB_REPO_URL,
8499
variant,
@@ -90,12 +105,36 @@ export function GitHubButton({
90105
keepBlack,
91106
}: GitHubButtonProps): JSX.Element {
92107
if (variant === "desktopPill") {
93-
return <DesktopPillVariant href={href} label={label} className={className} arrow={arrow} classes={classes} />;
108+
return (
109+
<DesktopPillVariant
110+
href={href}
111+
label={label}
112+
className={className}
113+
arrow={arrow}
114+
classes={classes}
115+
/>
116+
);
94117
}
95118
if (variant === "desktopGlow") {
96-
return <DesktopGlowVariant href={href} className={className} compact={compact} keepBlack={keepBlack} arrow={arrow} />;
119+
return (
120+
<DesktopGlowVariant
121+
href={href}
122+
className={className}
123+
compact={compact}
124+
keepBlack={keepBlack}
125+
arrow={arrow}
126+
/>
127+
);
97128
}
98-
return <MobileBannerVariant href={href} label={label} className={className} arrow={arrow} classes={classes} />;
129+
return (
130+
<MobileBannerVariant
131+
href={href}
132+
label={label}
133+
className={className}
134+
arrow={arrow}
135+
classes={classes}
136+
/>
137+
);
99138
}
100139

101140
// --- desktopPill (= HeroSection's DesktopGithubButton) ----------------------
@@ -113,7 +152,13 @@ function DesktopPillVariant({
113152
classes?: GitHubButtonProps["classes"];
114153
}): JSX.Element {
115154
return (
116-
<PillLink href={href} external className={cx(className)} arrow={arrow}>
155+
<PillLink
156+
href={href}
157+
external
158+
className={cx(className)}
159+
arrow={arrow}
160+
onClick={() => captureGitHubCta(href, "github_button_desktop_pill")}
161+
>
117162
<span aria-hidden="true" className={classes?.icon}>
118163
<GitHubIcon />
119164
</span>
@@ -151,6 +196,7 @@ function DesktopGlowVariant({
151196
className,
152197
)}
153198
aria-label={`Star us on GitHub — ${label} stars`}
199+
onClick={() => captureGitHubCta(href, "github_button_desktop_glow")}
154200
>
155201
<GitHubMark className={styles.glowSvg} />
156202
<span className={styles.glowCount} aria-hidden="true">
@@ -181,7 +227,13 @@ function MobileBannerVariant({
181227
const count = useGitHubStars(href);
182228

183229
return (
184-
<a href={href} target="_blank" rel="noopener noreferrer" className={cx(className)}>
230+
<a
231+
href={href}
232+
target="_blank"
233+
rel="noopener noreferrer"
234+
className={cx(className)}
235+
onClick={() => captureGitHubCta(href, "github_button_mobile_banner")}
236+
>
185237
<span className={classes?.lead}>
186238
<span aria-hidden="true" className={classes?.icon}>
187239
<GitHubIcon />

docs/app/(home)/page.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import type { Metadata } from "next";
1+
import {
2+
createIndexablePageMetadata,
3+
SITE_DESCRIPTION,
4+
SITE_IMAGE,
5+
SITE_TITLE,
6+
} from "@/lib/seo/metadata";
27
import styles from "./page.module.css";
38
import { CloudBanner } from "./sections/CloudBanner/CloudBanner";
49
import { CloudSection } from "./sections/CloudSection/CloudSection";
@@ -11,11 +16,13 @@ import { StepsSection } from "./sections/StepsSection/StepsSection";
1116
import { TweetWallSection } from "./sections/TweetWallSection/TweetWallSection";
1217
import { UseCasesSection } from "./sections/UseCasesSection/UseCasesSection";
1318

14-
export const metadata: Metadata = {
15-
alternates: {
16-
canonical: "/",
17-
},
18-
};
19+
export const metadata = createIndexablePageMetadata({
20+
pathname: "/",
21+
title: SITE_TITLE,
22+
absoluteTitle: true,
23+
description: SITE_DESCRIPTION,
24+
image: SITE_IMAGE,
25+
});
1926

2027
export default function HomePage() {
2128
return (

docs/app/(home)/sections/HeroSection/HeroSection.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
"use client";
22

3+
import { captureWebsiteEvent } from "@/lib/analytics/client";
34
import { captureCreateCliCommandCopied } from "@/lib/create-cli-copy-analytics";
45
import { ArrowRight } from "lucide-react";
5-
import { PLATFORMS } from "../../components/PlatformLogos";
66
import { useTheme } from "next-themes";
77
import { useEffect, useRef, useState, type ReactNode } from "react";
88
import { ClipboardCommandButton, PillLink } from "../../components/Button/Button";
9-
import {
10-
DEFAULT_GITHUB_REPO_URL,
11-
GitHubButton,
12-
} from "../../components/GitHubButton/GitHubButton";
9+
import { DEFAULT_GITHUB_REPO_URL, GitHubButton } from "../../components/GitHubButton/GitHubButton";
10+
import { PLATFORMS } from "../../components/PlatformLogos";
1311
import styles from "./HeroSection.module.css";
1412

1513
function capturePrimaryHeroCliCopy(command: string) {
@@ -46,7 +44,11 @@ function commandVariants(command: string): CommandVariant[] {
4644
const runner = COMMAND_RUNNERS.find(({ prefix }) => command.startsWith(`${prefix} `));
4745
if (!runner) return [];
4846
const spec = command.slice(runner.prefix.length + 1);
49-
return COMMAND_RUNNERS.map(({ id, prefix }) => ({ id, command: `${prefix} ${spec}`, runner: prefix }));
47+
return COMMAND_RUNNERS.map(({ id, prefix }) => ({
48+
id,
49+
command: `${prefix} ${spec}`,
50+
runner: prefix,
51+
}));
5052
}
5153

5254
const DESKTOP_HERO_IMAGE = {
@@ -233,6 +235,12 @@ function DesktopPlaygroundButton({ className = "" }: { className?: string }) {
233235
href="/demos"
234236
className={`${styles.desktopPlaygroundButton} ${className}`.trim()}
235237
arrow={<TrailingArrow />}
238+
onClick={() =>
239+
captureWebsiteEvent("demos_cta_clicked", {
240+
destination_url: "/demos",
241+
placement: "hero_desktop",
242+
})
243+
}
236244
>
237245
<span>{secondaryCTA}</span>
238246
</PillLink>
@@ -245,6 +253,12 @@ function MobilePlaygroundButton({ className = "" }: { className?: string }) {
245253
href="/demo/github"
246254
className={`${styles.mobilePlaygroundButton} ${className}`.trim()}
247255
arrow={<TrailingArrow />}
256+
onClick={() =>
257+
captureWebsiteEvent("demo_cta_clicked", {
258+
destination_url: "/demo/github",
259+
placement: "hero_mobile",
260+
})
261+
}
248262
>
249263
<span className={styles.mobilePlaygroundLabel}>Try Demo</span>
250264
</PillLink>

docs/app/blog/[slug]/page.tsx

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { GitHubButton } from "@/app/(home)/components/GitHubButton/GitHubButton";
22
import { Footer } from "@/app/(home)/sections/Footer/Footer";
3+
import { createIndexablePageMetadata } from "@/lib/seo/metadata";
34
import { blog } from "@/lib/source";
45
import { getMDXComponents } from "@/mdx-components";
5-
import { TOCItem, TOCItems } from "fumadocs-ui/components/toc/default";
66
import { TOCProvider, TOCScrollArea } from "fumadocs-ui/components/toc";
7+
import { TOCItem, TOCItems } from "fumadocs-ui/components/toc/default";
78
import type { Metadata } from "next";
89
import { notFound } from "next/navigation";
910

@@ -20,40 +21,40 @@ export default async function BlogPostPage(props: { params: Promise<{ slug: stri
2021
footer's surface — matching the home content band (light: foreground->background->foreground;
2122
dark: black -> neutral-950 -> black, so the edges meet the black menu + footer). */}
2223
<div className="min-h-screen bg-[linear-gradient(to_bottom,var(--openui-foreground),var(--openui-background)_10rem,var(--openui-background)_calc(100%_-_10rem),var(--openui-foreground))] [[data-theme=dark]_&]:bg-[linear-gradient(to_bottom,#000,var(--swatch-neutral-950)_28rem,var(--swatch-neutral-950)_calc(100%_-_28rem),#000)]">
23-
<main className="mx-auto flex w-full max-w-[var(--home-container-wide)] gap-4 px-4 pt-16 pb-40 lg:gap-28 lg:pr-8 min-[1249px]:pl-0 min-[1024px]:max-[1248px]:pl-8">
24-
<aside className="hidden w-56 shrink-0 lg:block">
25-
<div className="sticky top-24">
26-
<p className="mb-3 text-sm font-medium text-fd-foreground">On this page</p>
27-
<TOCScrollArea className="max-h-[calc(100vh-8rem)]">
28-
<TOCItems>
29-
{page.data.toc.map((item) => (
30-
<TOCItem key={item.url} item={item} />
31-
))}
32-
</TOCItems>
33-
</TOCScrollArea>
34-
</div>
35-
</aside>
24+
<main className="mx-auto flex w-full max-w-[var(--home-container-wide)] gap-4 px-4 pt-16 pb-40 lg:gap-28 lg:pr-8 min-[1249px]:pl-0 min-[1024px]:max-[1248px]:pl-8">
25+
<aside className="hidden w-56 shrink-0 lg:block">
26+
<div className="sticky top-24">
27+
<p className="mb-3 text-sm font-medium text-fd-foreground">On this page</p>
28+
<TOCScrollArea className="max-h-[calc(100vh-8rem)]">
29+
<TOCItems>
30+
{page.data.toc.map((item) => (
31+
<TOCItem key={item.url} item={item} />
32+
))}
33+
</TOCItems>
34+
</TOCScrollArea>
35+
</div>
36+
</aside>
3637

37-
<div className="min-w-0 flex-1">
38-
<h1 className="mb-2 text-[length:var(--home-title-size)] font-[family-name:var(--home-font-display)] font-medium leading-[var(--home-title-leading)] tracking-[var(--home-title-tracking)] text-[color:var(--openui-text-neutral-primary)]">
39-
{page.data.title}
40-
</h1>
41-
<p className="mb-4 text-[length:var(--home-lead-size)] font-[family-name:var(--home-font-text)] leading-[var(--home-lead-leading)] text-[color:var(--openui-text-neutral-secondary)]">
42-
{page.data.description}
43-
</p>
44-
<div className="flex items-center gap-3 border-b border-[color:var(--home-hairline)] pb-6 text-[length:var(--home-body-size)] font-[family-name:var(--home-font-text)] text-[color:var(--openui-text-neutral-tertiary)]">
45-
<span>{page.data.author}</span>
46-
<span>&middot;</span>
47-
<time>{new Date(page.data.date).toDateString()}</time>
48-
</div>
49-
<article className="prose mt-8 min-w-0">
50-
<Mdx components={getMDXComponents()} />
51-
</article>
52-
<div className="mt-12 border-t border-[color:var(--home-hairline)] pt-8">
53-
<GitHubButton variant="desktopGlow" />
38+
<div className="min-w-0 flex-1">
39+
<h1 className="mb-2 text-[length:var(--home-title-size)] font-[family-name:var(--home-font-display)] font-medium leading-[var(--home-title-leading)] tracking-[var(--home-title-tracking)] text-[color:var(--openui-text-neutral-primary)]">
40+
{page.data.title}
41+
</h1>
42+
<p className="mb-4 text-[length:var(--home-lead-size)] font-[family-name:var(--home-font-text)] leading-[var(--home-lead-leading)] text-[color:var(--openui-text-neutral-secondary)]">
43+
{page.data.description}
44+
</p>
45+
<div className="flex items-center gap-3 border-b border-[color:var(--home-hairline)] pb-6 text-[length:var(--home-body-size)] font-[family-name:var(--home-font-text)] text-[color:var(--openui-text-neutral-tertiary)]">
46+
<span>{page.data.author}</span>
47+
<span>&middot;</span>
48+
<time>{new Date(page.data.date).toDateString()}</time>
49+
</div>
50+
<article className="prose mt-8 min-w-0">
51+
<Mdx components={getMDXComponents()} />
52+
</article>
53+
<div className="mt-12 border-t border-[color:var(--home-hairline)] pt-8">
54+
<GitHubButton variant="desktopGlow" />
55+
</div>
5456
</div>
55-
</div>
56-
</main>
57+
</main>
5758
</div>
5859
<Footer />
5960
</TOCProvider>
@@ -68,13 +69,14 @@ export async function generateMetadata(props: {
6869

6970
if (!page) notFound();
7071

71-
return {
72+
return createIndexablePageMetadata({
73+
pathname: page.url,
7274
title: page.data.title,
7375
description: page.data.description,
74-
alternates: {
75-
canonical: page.url,
76-
},
77-
};
76+
image: `/images/blog/${params.slug}.png`,
77+
openGraphType: "article",
78+
publishedTime: new Date(page.data.date).toISOString(),
79+
});
7880
}
7981

8082
export function generateStaticParams(): { slug: string }[] {

0 commit comments

Comments
 (0)