Landing page redesign + copy/positioning doc (#267)
@ -18,7 +18,7 @@ If you want an agent to help, connect [OpenSEO MCP](/docs/mcp) first so it can u
|
||||
- [How can you improve your site's rankings?](#how-can-you-improve-your-sites-rankings)
|
||||
- [A simple founder SEO priority list](#a-simple-founder-seo-priority-list)
|
||||
|
||||

|
||||

|
||||
|
||||
## Mental Model: How does SEO affect your business?
|
||||
|
||||
@ -62,7 +62,7 @@ That can shape your strategy. If a competitor gets traffic from a specific page
|
||||
|
||||
For a focused workflow, use the [competitor analysis skill](/docs/skills/competitor-analysis). If you are still figuring out which domains matter, start with the broader [competitive landscape skill](/docs/skills/competitive-landscape).
|
||||
|
||||

|
||||

|
||||
|
||||
### Keyword Research
|
||||
|
||||
@ -72,7 +72,7 @@ It also helps map a market. If you search a seed phrase like "best crm for," the
|
||||
|
||||
For a repeatable workflow, use the [keyword research skill](/docs/skills/keyword-research). When you have a useful list of ideas, use the [keyword clustering skill](/docs/skills/keyword-clustering) to group them into page targets.
|
||||
|
||||

|
||||

|
||||
|
||||
### Backlink Analysis
|
||||
|
||||
@ -82,7 +82,7 @@ When you look at who links to competitors, you start to see which publications,
|
||||
|
||||
Those links can tell you how attention moves through a market.
|
||||
|
||||

|
||||

|
||||
|
||||
## How can you improve your site's rankings?
|
||||
|
||||
|
||||
BIN
web/public/avatars/fed-avatar.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
web/public/avatars/samik-avatar.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
web/public/avatars/tom-avatar.jpeg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
web/public/blog/seo-for-startups/backlink-analysis.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
web/public/blog/seo-for-startups/domain-overview.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
web/public/blog/seo-for-startups/infographic.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
web/public/blog/seo-for-startups/keyword-research.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 10 MiB |
56
web/src/components/blog-layout.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
const navLinks = [
|
||||
{ label: "Features", to: "/features" },
|
||||
{ label: "Blog", to: "/blogs" },
|
||||
{ label: "Docs", to: "/docs" },
|
||||
{ label: "Pricing", to: "/pricing" },
|
||||
] as const;
|
||||
|
||||
export function BlogLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--color-surface)] text-neutral-950">
|
||||
<header className="border-b border-[var(--color-border-subtle)] bg-[var(--color-surface)]">
|
||||
<div className="mx-auto flex h-16 w-full max-w-6xl items-center justify-between gap-4 px-5 sm:px-6">
|
||||
<Link
|
||||
to="/"
|
||||
className="shrink-0 text-base font-semibold text-neutral-950 transition-opacity hover:opacity-80"
|
||||
>
|
||||
OpenSEO
|
||||
</Link>
|
||||
|
||||
<nav
|
||||
aria-label="Blog navigation"
|
||||
className="flex min-w-0 items-center justify-end gap-4 overflow-x-auto text-sm font-medium text-[var(--color-brand-muted)] sm:gap-6"
|
||||
>
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.to}
|
||||
to={link.to}
|
||||
className={`shrink-0 transition-colors hover:text-neutral-950 ${
|
||||
link.label === "Features" || link.label === "Pricing"
|
||||
? "hidden sm:inline"
|
||||
: ""
|
||||
}`}
|
||||
activeProps={{ className: "text-neutral-950" }}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
<a
|
||||
href="https://github.com/every-app/open-seo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hidden shrink-0 transition-colors hover:text-neutral-950 sm:inline"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -77,7 +77,7 @@ const ROWS: { label: string; cells: Cell[] }[] = [
|
||||
export function ComparisonTable() {
|
||||
return (
|
||||
<div className="not-prose my-8">
|
||||
<div className="overflow-x-auto rounded-xl border border-neutral-200">
|
||||
<div className="overflow-x-auto rounded-xl border border-[var(--color-border-subtle)] bg-white">
|
||||
<table className="w-full min-w-[680px] border-collapse text-left">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -88,7 +88,7 @@ export function ComparisonTable() {
|
||||
scope="col"
|
||||
className={`p-4 align-bottom text-sm font-semibold ${
|
||||
col.highlight
|
||||
? "border-x border-neutral-200 bg-neutral-50 text-neutral-950"
|
||||
? "border-x border-[var(--color-border-subtle)] bg-[#fbfaf8] text-neutral-950"
|
||||
: "text-neutral-900"
|
||||
}`}
|
||||
>
|
||||
@ -102,7 +102,7 @@ export function ComparisonTable() {
|
||||
<tr key={row.label}>
|
||||
<th
|
||||
scope="row"
|
||||
className="border-t border-neutral-200 p-4 align-top text-sm font-medium text-neutral-500"
|
||||
className="border-t border-[var(--color-border-subtle)] p-4 align-top text-sm font-medium text-[var(--color-brand-muted)]"
|
||||
>
|
||||
{row.label}
|
||||
</th>
|
||||
@ -111,8 +111,10 @@ export function ComparisonTable() {
|
||||
return (
|
||||
<td
|
||||
key={COLUMNS[i]?.name ?? i}
|
||||
className={`border-t border-neutral-200 p-4 align-top text-sm ${
|
||||
highlight ? "border-x bg-neutral-50" : ""
|
||||
className={`border-t border-[var(--color-border-subtle)] p-4 align-top text-sm ${
|
||||
highlight
|
||||
? "border-x border-[var(--color-border-subtle)] bg-[#fbfaf8]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<CellContent cell={cell} highlight={highlight} />
|
||||
@ -141,14 +143,16 @@ function CellContent({ cell, highlight }: { cell: Cell; highlight?: boolean }) {
|
||||
<div className="flex items-start gap-2.5">
|
||||
<span
|
||||
className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center ${
|
||||
tone === "negative" ? "text-neutral-300" : "text-neutral-900"
|
||||
tone === "negative"
|
||||
? "text-neutral-300"
|
||||
: "text-[var(--color-brand-accent)]"
|
||||
}`}
|
||||
>
|
||||
<ToneIcon tone={tone} />
|
||||
</span>
|
||||
<span className={`leading-snug ${textClass}`}>
|
||||
{cell.code ? (
|
||||
<code className="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-[0.85em] text-neutral-800">
|
||||
<code className="rounded bg-[#ebe4da] px-1.5 py-0.5 font-mono text-[0.85em] text-neutral-800">
|
||||
{cell.text}
|
||||
</code>
|
||||
) : (
|
||||
|
||||
@ -6,21 +6,26 @@ type FeaturePageProps = {
|
||||
|
||||
export function FeaturePageTemplate({ page }: FeaturePageProps) {
|
||||
return (
|
||||
<article>
|
||||
<header>
|
||||
<p className="text-sm font-medium text-neutral-500">{page.eyebrow}</p>
|
||||
<h1 className="mt-3 text-3xl font-bold leading-tight tracking-tight">
|
||||
<article className="mx-auto max-w-5xl">
|
||||
<header className="max-w-3xl">
|
||||
<p className="text-sm font-medium text-[var(--color-brand-accent)]">
|
||||
{page.eyebrow}
|
||||
</p>
|
||||
<h1 className="mt-3 text-4xl font-semibold leading-tight tracking-tight text-neutral-950 md:text-6xl">
|
||||
{page.title}
|
||||
</h1>
|
||||
<p className="mt-4 text-neutral-700 leading-relaxed">
|
||||
<p className="mt-5 text-lg leading-8 text-[var(--color-brand-muted)]">
|
||||
{page.description}
|
||||
</p>
|
||||
<div className="mt-5">
|
||||
<a
|
||||
href="https://app.openseo.so/sign-up"
|
||||
className="inline-flex h-10 items-center justify-center rounded-md bg-neutral-900 px-5 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
|
||||
className="inline-flex h-11 items-center justify-center rounded-lg bg-neutral-950 px-5 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
|
||||
>
|
||||
Try OpenSEO
|
||||
<span aria-hidden="true" className="ml-2">
|
||||
→
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
@ -28,87 +33,52 @@ export function FeaturePageTemplate({ page }: FeaturePageProps) {
|
||||
<FeatureImage page={page} />
|
||||
|
||||
<section className="mt-12">
|
||||
<h2 className="text-xl font-semibold">What you can do</h2>
|
||||
<ol className="mt-6 space-y-6">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
|
||||
What you can do
|
||||
</h2>
|
||||
<ol className="mt-5 grid gap-4 md:grid-cols-3">
|
||||
{page.workflows.map((workflow, index) => (
|
||||
<li
|
||||
key={workflow.title}
|
||||
className="grid grid-cols-[2.25rem_1fr] gap-x-4"
|
||||
className="rounded-lg border border-[var(--color-border-subtle)] bg-white p-5"
|
||||
>
|
||||
<span className="pt-[2px] font-mono text-sm tabular-nums text-neutral-400">
|
||||
<span className="font-mono text-sm tabular-nums text-[var(--color-brand-accent)]">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-neutral-900">
|
||||
<h3 className="mt-4 text-base font-semibold text-neutral-950">
|
||||
{workflow.title}
|
||||
</h3>
|
||||
<p className="mt-1.5 text-sm leading-relaxed text-neutral-600">
|
||||
<p className="mt-2 text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
{workflow.description}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section className="mt-12">
|
||||
<h2 className="text-xl font-semibold">Data you can act on</h2>
|
||||
<dl className="mt-5 grid grid-cols-2 border-y border-neutral-200 md:grid-cols-4 md:divide-x md:divide-neutral-200">
|
||||
{page.metrics.map((metric, index) => (
|
||||
<div
|
||||
key={metric.label}
|
||||
className={[
|
||||
"px-4 py-3",
|
||||
index % 2 === 1 && "border-l border-neutral-200 md:border-l-0",
|
||||
index < 2 && "border-b border-neutral-200 md:border-b-0",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
<dt className="text-xs text-neutral-500">{metric.label}</dt>
|
||||
<dd className="mt-1 text-sm font-semibold text-neutral-900">
|
||||
{metric.value}
|
||||
</dd>
|
||||
{page.showMetrics ? <MetricsSection page={page} /> : null}
|
||||
|
||||
<div className="mt-12 grid gap-5 md:grid-cols-2">
|
||||
<ListSection title="Use cases" items={page.useCases} />
|
||||
<ListSection title="Why OpenSEO" items={page.differentiators} />
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section className="mt-12">
|
||||
<h2 className="text-xl font-semibold">Use cases</h2>
|
||||
<ul className="mt-4 space-y-3">
|
||||
{page.useCases.map((item) => (
|
||||
<li key={item} className="flex gap-2.5 text-sm text-neutral-700">
|
||||
<span className="mt-[2px] text-neutral-400">—</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mt-12">
|
||||
<h2 className="text-xl font-semibold">Why OpenSEO</h2>
|
||||
<ul className="mt-4 space-y-3">
|
||||
{page.differentiators.map((item) => (
|
||||
<li key={item} className="flex gap-2.5 text-sm text-neutral-700">
|
||||
<span className="mt-[2px] text-neutral-400">—</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mt-12">
|
||||
<h2 className="text-xl font-semibold">Related features</h2>
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
|
||||
Related features
|
||||
</h2>
|
||||
<div className="mt-4 grid gap-3 md:grid-cols-3">
|
||||
{page.related.map((item) => (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="rounded-lg border border-neutral-200 bg-white p-4 text-sm font-medium text-neutral-900 transition-colors hover:border-neutral-900"
|
||||
className="rounded-lg border border-[var(--color-border-subtle)] bg-white p-4 text-sm font-medium text-neutral-950 transition-colors hover:border-neutral-900"
|
||||
>
|
||||
{item.label}
|
||||
<span aria-hidden="true" className="ml-1 text-neutral-500">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="ml-1 text-[var(--color-brand-accent)]"
|
||||
>
|
||||
→
|
||||
</span>
|
||||
</a>
|
||||
@ -117,14 +87,16 @@ export function FeaturePageTemplate({ page }: FeaturePageProps) {
|
||||
</section>
|
||||
|
||||
<section className="mt-12">
|
||||
<h2 className="text-xl font-semibold">FAQ</h2>
|
||||
<div className="mt-5 divide-y divide-neutral-200 rounded-lg border border-neutral-200 bg-white">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
|
||||
FAQ
|
||||
</h2>
|
||||
<div className="mt-5 divide-y divide-[var(--color-border-subtle)] rounded-lg border border-[var(--color-border-subtle)] bg-white">
|
||||
{page.faqs.map((faq) => (
|
||||
<div key={faq.question} className="p-4">
|
||||
<div key={faq.question} className="p-5">
|
||||
<h3 className="text-sm font-semibold text-neutral-900">
|
||||
{faq.question}
|
||||
</h3>
|
||||
<p className="mt-1.5 text-sm leading-relaxed text-neutral-600">
|
||||
<p className="mt-1.5 text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
{faq.answer}
|
||||
</p>
|
||||
</div>
|
||||
@ -132,17 +104,22 @@ export function FeaturePageTemplate({ page }: FeaturePageProps) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-lg border border-neutral-200 bg-neutral-50 p-5">
|
||||
<h2 className="text-lg font-semibold text-neutral-900">Try OpenSEO</h2>
|
||||
<p className="mt-2 text-sm leading-relaxed text-neutral-600">
|
||||
<section className="mt-12 rounded-xl border border-[var(--color-border-subtle)] bg-white p-6 md:p-8">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
|
||||
Try OpenSEO
|
||||
</h2>
|
||||
<p className="mt-2 max-w-2xl text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
The open source alternative to bloated, expensive, legacy SEO tools.
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<a
|
||||
href="https://app.openseo.so/sign-up"
|
||||
className="inline-flex h-9 items-center justify-center rounded-md bg-neutral-900 px-4 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
|
||||
className="inline-flex h-10 items-center justify-center rounded-lg bg-neutral-950 px-4 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
|
||||
>
|
||||
Try OpenSEO
|
||||
<span aria-hidden="true" className="ml-2">
|
||||
→
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
@ -152,7 +129,7 @@ export function FeaturePageTemplate({ page }: FeaturePageProps) {
|
||||
|
||||
function FeatureImage({ page }: FeaturePageProps) {
|
||||
return (
|
||||
<figure className="mt-9">
|
||||
<figure className="mt-10 rounded-xl border border-[var(--color-border-subtle)] bg-white p-3">
|
||||
<img
|
||||
src={page.imageSrc}
|
||||
alt={page.imageAlt}
|
||||
@ -160,11 +137,67 @@ function FeatureImage({ page }: FeaturePageProps) {
|
||||
height={1000}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
className="aspect-[16/10] w-full rounded-lg border border-neutral-200 object-cover object-top"
|
||||
className="aspect-[16/10] w-full rounded-lg border border-[#ebe4da] object-cover object-top"
|
||||
/>
|
||||
<figcaption className="mt-2 text-[11px] text-neutral-500">
|
||||
<figcaption className="px-1 pt-2 text-[11px] text-[var(--color-brand-muted)]">
|
||||
{page.eyebrow} in OpenSEO.
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricsSection({ page }: FeaturePageProps) {
|
||||
return (
|
||||
<section className="mt-12">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
|
||||
Data you can act on
|
||||
</h2>
|
||||
<dl className="mt-5 grid overflow-hidden rounded-lg border border-[var(--color-border-subtle)] bg-white sm:grid-cols-2 md:grid-cols-4">
|
||||
{page.metrics.map((metric, index) => (
|
||||
<div
|
||||
key={metric.label}
|
||||
className={[
|
||||
"p-5",
|
||||
index > 0 && "border-t border-[var(--color-border-subtle)]",
|
||||
index % 2 === 1 &&
|
||||
"sm:border-l sm:border-[var(--color-border-subtle)]",
|
||||
index > 1 && "sm:border-t",
|
||||
index > 0 &&
|
||||
"md:border-l md:border-t-0 md:border-[var(--color-border-subtle)]",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
<dt className="text-xs text-[var(--color-brand-muted)]">
|
||||
{metric.label}
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm font-semibold text-neutral-950">
|
||||
{metric.value}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ListSection({ title, items }: { title: string; items: string[] }) {
|
||||
return (
|
||||
<section className="rounded-lg border border-[var(--color-border-subtle)] bg-white p-5">
|
||||
<h2 className="text-xl font-semibold tracking-tight text-neutral-950">
|
||||
{title}
|
||||
</h2>
|
||||
<ul className="mt-4 space-y-3">
|
||||
{items.map((item) => (
|
||||
<li key={item} className="flex gap-2.5 text-sm text-neutral-700">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="mt-[7px] h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--color-brand-accent)]"
|
||||
/>
|
||||
<span className="leading-6">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
691
web/src/components/landing-page.css
Normal file
@ -0,0 +1,691 @@
|
||||
/**
|
||||
* Landing-page styles.
|
||||
*
|
||||
* Scoped under `.itc` and kept unlayered (plain CSS) so these component
|
||||
* classes win over Tailwind's layered utilities, matching the page's design.
|
||||
* Palette vars that exactly match the global @theme tokens (app.css) alias
|
||||
* them instead of redefining the hex; vars with bespoke values stay local.
|
||||
*/
|
||||
|
||||
.itc {
|
||||
--canvas: var(--color-surface);
|
||||
--surface: var(--color-surface-raised);
|
||||
--surface-2: #ebe7e1;
|
||||
--ink: var(--color-brand);
|
||||
--ink-muted: #626260;
|
||||
--ink-subtle: #7b7b78;
|
||||
--ink-tertiary: #9c9fa5;
|
||||
--hairline: #d3cec6;
|
||||
--hairline-soft: #ebe7e1;
|
||||
--orange: var(--color-brand-accent);
|
||||
--inverse: #000000;
|
||||
--inverse-surface: #313130;
|
||||
--inverse-muted: #9c9fa5;
|
||||
/* Testimonial band (light: deep greige band, white cards, charcoal text) */
|
||||
--q-bg: #e3dccd;
|
||||
--q-card: #ffffff;
|
||||
--q-card-border: #d0c9bb;
|
||||
--q-text: #111111;
|
||||
--q-muted: #7b7b78;
|
||||
--q-avatar-bg: #111111;
|
||||
--q-avatar-fg: #ffffff;
|
||||
--q-mark: #b8ac96;
|
||||
/* Dark "emphasis" surfaces: featured pricing card + MCP terminal */
|
||||
--feat-bg: #221f1b;
|
||||
--feat-text: #ffffff;
|
||||
--term-bg: #221f1b;
|
||||
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
background: var(--canvas);
|
||||
color: var(--ink);
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
.itc *,
|
||||
.itc *::before,
|
||||
.itc *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.itc img,
|
||||
.itc video {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
/* Zero-specificity reset so component classes below always win. */
|
||||
.itc :where(a) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.itc-container {
|
||||
max-width: 1232px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
.itc-narrow {
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.itc-section {
|
||||
padding: 96px 0;
|
||||
}
|
||||
.itc-section-open-source {
|
||||
background: #f8f6f2;
|
||||
border-top: 1px solid var(--hairline);
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
}
|
||||
.itc-section-demo {
|
||||
background: var(--canvas);
|
||||
}
|
||||
.itc-section-demo + .itc-footer {
|
||||
margin-top: 0;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.itc-section {
|
||||
padding: 64px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Type scale (Inter standing in for Saans; weight 500 carries display) ── */
|
||||
.itc-display-xl {
|
||||
font-size: clamp(40px, 6.5vw, 72px);
|
||||
font-weight: 500;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.028em;
|
||||
margin: 0;
|
||||
text-wrap: balance;
|
||||
}
|
||||
/* Desktop keeps the headline on two lines via a forced break; mobile wraps
|
||||
naturally (the break is removed below the tablet breakpoint). */
|
||||
.itc-h1-br {
|
||||
display: inline;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.itc-h1-br {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
/* Let the forced <br> own the break; don't let balance re-split into 3 lines. */
|
||||
.itc-display-xl {
|
||||
text-wrap: wrap;
|
||||
}
|
||||
}
|
||||
.itc-display-lg {
|
||||
font-size: clamp(32px, 4.4vw, 52px);
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.026em;
|
||||
margin: 0;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.itc-display-md {
|
||||
font-size: clamp(28px, 3.4vw, 40px);
|
||||
font-weight: 500;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.itc-card-title {
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -0.013em;
|
||||
margin: 0;
|
||||
}
|
||||
.itc-subhead {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.itc-body-lg {
|
||||
font-size: 18px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.005em;
|
||||
}
|
||||
.itc-body-sm {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.itc-caption {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.itc-eyebrow {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0;
|
||||
margin: 0 0 16px;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.itc-muted {
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.itc-subtle {
|
||||
color: var(--ink-subtle);
|
||||
}
|
||||
@media (max-width: 479px) {
|
||||
.itc-subhead {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Buttons: 8px radius, 10x18 padding, never pill ── */
|
||||
.itc-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 44px;
|
||||
padding: 10px 18px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
font-family: inherit;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
}
|
||||
.itc-btn:focus-visible {
|
||||
outline: 2px solid var(--ink);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.itc-btn-lg {
|
||||
min-height: 54px;
|
||||
padding: 15px 28px;
|
||||
font-size: 17px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.itc-btn-primary {
|
||||
background: var(--ink);
|
||||
color: #ffffff;
|
||||
}
|
||||
.itc-btn-primary:hover {
|
||||
background: #000000;
|
||||
}
|
||||
.itc-btn-secondary {
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
border-color: var(--hairline);
|
||||
}
|
||||
.itc-btn-secondary:hover {
|
||||
border-color: var(--ink);
|
||||
}
|
||||
.itc-btn-fin {
|
||||
background: var(--orange);
|
||||
color: #ffffff;
|
||||
}
|
||||
.itc-btn-fin:hover {
|
||||
background: #e64d00;
|
||||
}
|
||||
.itc-btn-fin:focus-visible {
|
||||
outline-color: var(--orange);
|
||||
}
|
||||
.itc-arrow {
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.itc-btn:hover .itc-arrow,
|
||||
.itc-textlink:hover .itc-arrow {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.itc-arrow,
|
||||
.itc-btn,
|
||||
.itc-textlink {
|
||||
transition: none;
|
||||
}
|
||||
.itc-btn:hover .itc-arrow,
|
||||
.itc-textlink:hover .itc-arrow {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.itc-textlink {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.itc-textlink:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
.itc-textlink:focus-visible {
|
||||
outline: 2px solid var(--ink);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ── Surfaces: white lift on cream, hairline borders, no shadows ── */
|
||||
.itc-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 12px;
|
||||
}
|
||||
.itc-mockup {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 16px;
|
||||
padding: 12px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.itc-mockup {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
.itc-mockup-media {
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--hairline-soft);
|
||||
overflow: hidden;
|
||||
background: var(--canvas);
|
||||
}
|
||||
|
||||
/* ── Hero ── */
|
||||
.itc-hero {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
min-height: calc(100svh - 150px);
|
||||
padding-top: clamp(104px, 12vh, 176px);
|
||||
padding-bottom: clamp(96px, 12vh, 144px);
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
.itc-hero::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
transition: background-color 0.18s ease;
|
||||
}
|
||||
.itc-hero .itc-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.itc-hero-title {
|
||||
color: var(--ink);
|
||||
transition: color 0.18s ease;
|
||||
}
|
||||
.itc-hero-title-accent {
|
||||
color: inherit;
|
||||
-webkit-box-decoration-break: clone;
|
||||
box-decoration-break: clone;
|
||||
}
|
||||
.itc-hero-subtitle {
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
color 0.18s ease;
|
||||
}
|
||||
.itc-hero-subtitle-counter,
|
||||
.itc-hero-subtitle-win {
|
||||
-webkit-box-decoration-break: clone;
|
||||
box-decoration-break: clone;
|
||||
font-style: italic;
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
color 0.18s ease,
|
||||
text-decoration-color 0.18s ease;
|
||||
}
|
||||
.itc-hero-subtitle-counter {
|
||||
color: #7a746d;
|
||||
}
|
||||
.itc-hero-subtitle-win {
|
||||
color: var(--orange);
|
||||
font-weight: 500;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.itc-hero {
|
||||
min-height: calc(100svh - 120px);
|
||||
padding-top: 72px;
|
||||
padding-bottom: 88px;
|
||||
}
|
||||
}
|
||||
.itc-hero-ctas {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
@media (max-width: 479px) {
|
||||
.itc-hero-ctas {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Testimonial band (colors driven by --q-* palette vars) ── */
|
||||
.itc-inverse {
|
||||
background: var(--q-bg);
|
||||
color: var(--q-text);
|
||||
}
|
||||
.itc-testimonials {
|
||||
padding: 72px 0;
|
||||
}
|
||||
.itc-testimonials-title {
|
||||
max-width: none;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.itc-quote-mark {
|
||||
color: var(--q-mark);
|
||||
font-size: 48px;
|
||||
font-weight: 500;
|
||||
line-height: 0.5;
|
||||
}
|
||||
.itc-quote-grid {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.itc-quote-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.itc-testimonials {
|
||||
padding: 56px 0;
|
||||
}
|
||||
.itc-testimonials-title {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
.itc-quote-card {
|
||||
margin: 0;
|
||||
background: var(--q-card);
|
||||
border: 1px solid var(--q-card-border);
|
||||
border-radius: 12px;
|
||||
padding: 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.itc-quote-text {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--q-text);
|
||||
text-wrap: pretty;
|
||||
flex: 1;
|
||||
}
|
||||
.itc-quote-attr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 13px;
|
||||
}
|
||||
.itc-quote-avatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 9999px;
|
||||
background: var(--q-avatar-bg);
|
||||
color: var(--q-avatar-fg);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
flex: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.itc-quote-avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.itc-quote-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.itc-quote-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--q-text);
|
||||
}
|
||||
.itc-quote-handle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--q-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
.itc-quote-handle:hover {
|
||||
color: var(--q-text);
|
||||
}
|
||||
.itc-quote-handle:focus-visible {
|
||||
outline: 2px solid var(--q-text);
|
||||
outline-offset: 3px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ── Product grid ── */
|
||||
.itc-feature-grid {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.itc-feature-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
.itc-feature-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
text-decoration: none;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
.itc-feature-card:hover {
|
||||
border-color: var(--ink);
|
||||
}
|
||||
.itc-feature-card:focus-visible {
|
||||
outline: 2px solid var(--ink);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.itc-feature-card-body {
|
||||
padding: 16px 8px 8px;
|
||||
}
|
||||
.itc-feature-list-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.itc-feature-list-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
.itc-feature-list-card {
|
||||
min-height: 128px;
|
||||
padding: 20px;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
.itc-feature-list-card:hover {
|
||||
border-color: var(--ink);
|
||||
}
|
||||
.itc-feature-list-card:focus-visible {
|
||||
outline: 2px solid var(--ink);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.itc-feature-more-header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
/* ── MCP (the single orange moment) ── */
|
||||
.itc-mcp-section {
|
||||
background: #eee8de;
|
||||
border-top: 1px solid var(--hairline);
|
||||
padding: 84px 0 92px;
|
||||
}
|
||||
.itc-hero + .itc-mcp-section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.itc-mcp-grid {
|
||||
display: grid;
|
||||
gap: 48px;
|
||||
align-items: center;
|
||||
}
|
||||
@media (min-width: 980px) {
|
||||
.itc-mcp-grid {
|
||||
grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
|
||||
gap: 64px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.itc-mcp-section {
|
||||
padding: 64px 0;
|
||||
}
|
||||
.itc-hero + .itc-mcp-section {
|
||||
margin-top: 28px;
|
||||
}
|
||||
}
|
||||
.itc-terminal {
|
||||
background: var(--term-bg);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
.itc-terminal-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #2a2a29;
|
||||
}
|
||||
.itc-terminal-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 9999px;
|
||||
background: #313130;
|
||||
}
|
||||
.itc-terminal-label {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--inverse-muted);
|
||||
}
|
||||
.itc-terminal pre {
|
||||
margin: 0;
|
||||
padding: 20px 20px 24px;
|
||||
overflow-x: auto;
|
||||
font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: #d6d3ce;
|
||||
}
|
||||
.itc-terminal .t-orange {
|
||||
color: var(--orange);
|
||||
}
|
||||
.itc-terminal .t-bright {
|
||||
color: #ffffff;
|
||||
}
|
||||
.itc-terminal .t-dim {
|
||||
color: var(--inverse-muted);
|
||||
}
|
||||
.itc-agent-icons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
.itc-agent-icon {
|
||||
align-items: center;
|
||||
background: var(--surface-2);
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
height: 44px;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
width: 44px;
|
||||
}
|
||||
.itc-agent-icon svg {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
/* ── Footer ── */
|
||||
.itc-footer {
|
||||
background: #eee8de;
|
||||
border-top: 1px solid var(--hairline);
|
||||
}
|
||||
.itc-sitefooter {
|
||||
font-size: 14px;
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
.itc-sitefooter a {
|
||||
color: var(--ink-muted);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
.itc-sitefooter a:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
.itc-newsletter input {
|
||||
border-color: var(--hairline) !important;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
.itc-newsletter input:focus {
|
||||
border-color: var(--ink) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.itc-newsletter button {
|
||||
border-radius: 8px !important;
|
||||
background: var(--ink) !important;
|
||||
}
|
||||
|
||||
/* ── Floating Discord pill (quiet messenger-style nod) ── */
|
||||
.itc-discord {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
z-index: 40;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--ink);
|
||||
color: #ffffff;
|
||||
border-radius: 9999px;
|
||||
padding: 12px 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
.itc-discord:hover {
|
||||
background: #000000;
|
||||
}
|
||||
.itc-discord:focus-visible {
|
||||
outline: 2px solid var(--ink);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.itc-discord span {
|
||||
display: none;
|
||||
}
|
||||
.itc-discord {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
915
web/src/components/landing-page.tsx
Normal file
@ -0,0 +1,915 @@
|
||||
/**
|
||||
* Canonical marketing landing page.
|
||||
*
|
||||
* The page uses a soft cream canvas, restrained hairline surfaces, charcoal
|
||||
* type, and a single orange emphasis moment around the MCP section.
|
||||
*/
|
||||
|
||||
import { type ReactNode, type SVGProps } from "react";
|
||||
import { NewsletterSignup } from "@/components/newsletter-signup";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { featurePages } from "@/lib/feature-pages";
|
||||
import "./landing-page.css";
|
||||
|
||||
const SIGNUP_URL = "https://app.openseo.so/sign-up";
|
||||
const GITHUB_URL = "https://github.com/every-app/open-seo";
|
||||
const DISCORD_URL = "https://discord.gg/c9uGs3cFXr";
|
||||
|
||||
type Testimonial = {
|
||||
quote: string;
|
||||
name: string;
|
||||
initial: string;
|
||||
handle: string;
|
||||
href: string;
|
||||
network: "x" | "linkedin" | "web";
|
||||
avatarSrc?: string;
|
||||
};
|
||||
|
||||
const TESTIMONIALS: Testimonial[] = [
|
||||
{
|
||||
quote:
|
||||
"All of the value, none of the bloat. OpenSEO is a no-brainer compared to the expensive alternatives!",
|
||||
name: "Fed",
|
||||
initial: "F",
|
||||
handle: "@foliofed",
|
||||
href: "https://x.com/foliofed",
|
||||
network: "x",
|
||||
avatarSrc: "/avatars/fed-avatar.jpg",
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"I've been using OpenSEO for the past 3 months, Ben keeps launching features to make it the best. I use it every day to find where my competitors are ranking.",
|
||||
name: "Samik",
|
||||
initial: "S",
|
||||
handle: "Subclip",
|
||||
href: "https://www.subclip.app/",
|
||||
network: "web",
|
||||
avatarSrc: "/avatars/samik-avatar.jpg",
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"It's so straightforward and incredibly easy to get started. OpenSEO gives you the complete setup, stripped of all the fluff that you get elsewhere.",
|
||||
name: "Tom Raine",
|
||||
initial: "T",
|
||||
handle: "LinkedIn",
|
||||
href: "https://www.linkedin.com/in/tom-raine-hk/",
|
||||
network: "linkedin",
|
||||
avatarSrc: "/avatars/tom-avatar.jpeg",
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Icons (inline SVG only, per project convention) ─────────────────
|
||||
|
||||
type IconProps = { size?: number; className?: string };
|
||||
|
||||
function strokeProps(size: number, className?: string) {
|
||||
return {
|
||||
width: size,
|
||||
height: size,
|
||||
viewBox: "0 0 24 24",
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
strokeWidth: 2,
|
||||
strokeLinecap: "round" as const,
|
||||
strokeLinejoin: "round" as const,
|
||||
className,
|
||||
"aria-hidden": true,
|
||||
};
|
||||
}
|
||||
|
||||
function IconArrowRight({ size = 16, className }: IconProps) {
|
||||
return (
|
||||
<svg {...strokeProps(size, className)}>
|
||||
<path d="M5 12h14" />
|
||||
<path d="m12 5 7 7-7 7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconGithub({ size = 16, className }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconX({ size = 14, className }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.66l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconLinkedIn({ size = 14, className }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.34V9h3.42v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.46v6.28zM5.34 7.43a2.07 2.07 0 110-4.14 2.07 2.07 0 010 4.14zM7.12 20.45H3.56V9h3.56v11.45zM22.22 0H1.77C.79 0 0 .77 0 1.73v20.54C0 23.22.79 24 1.77 24h20.45c.98 0 1.78-.78 1.78-1.73V1.73C24 .77 23.2 0 22.22 0z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconLink({ size = 14, className }: IconProps) {
|
||||
return (
|
||||
<svg {...strokeProps(size, className)}>
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconDiscord({ size = 18, className }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Shared bits ─────────────────────────────────────────────────────
|
||||
|
||||
function Container({
|
||||
children,
|
||||
className = "",
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={`itc-container ${className}`}>{children}</div>;
|
||||
}
|
||||
|
||||
function ArrowCta({
|
||||
href = SIGNUP_URL,
|
||||
className = "itc-btn itc-btn-primary",
|
||||
children = "Start enjoying SEO",
|
||||
size = "md",
|
||||
}: {
|
||||
href?: string;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
size?: "md" | "lg";
|
||||
}) {
|
||||
return (
|
||||
<a href={href} className={`${className}${size === "lg" ? " itc-btn-lg" : ""}`}>
|
||||
{children}
|
||||
<IconArrowRight size={size === "lg" ? 18 : 16} className="itc-arrow" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Hero ────────────────────────────────────────────────────────────
|
||||
|
||||
function Hero() {
|
||||
return (
|
||||
<section className="itc-hero">
|
||||
<Container>
|
||||
<h1
|
||||
className="itc-display-xl itc-hero-title"
|
||||
style={{ maxWidth: 1180, margin: "0 auto" }}
|
||||
>
|
||||
Say goodbye to{" "}
|
||||
<span className="itc-hero-title-accent">bloated,</span>{" "}
|
||||
<br className="itc-h1-br" />
|
||||
<span className="itc-hero-title-accent">overpriced SEO tools.</span>
|
||||
</h1>
|
||||
<p
|
||||
className="itc-subhead itc-muted itc-hero-subtitle"
|
||||
style={{ maxWidth: 640, margin: "24px auto 0" }}
|
||||
>
|
||||
Speed up strategy decisions with quality SEO data for every workflow,
|
||||
all in one place.{" "}
|
||||
<span className="itc-hero-subtitle-counter">
|
||||
Don't put SEO on autopilot
|
||||
</span>
|
||||
,{" "}
|
||||
<span className="itc-hero-subtitle-win">
|
||||
work with your AI agent to actually win.
|
||||
</span>
|
||||
</p>
|
||||
<div className="itc-hero-ctas">
|
||||
<ArrowCta size="lg" />
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Testimonial (true-black inverse strip) ──────────────────────────
|
||||
|
||||
function Testimonial() {
|
||||
return (
|
||||
<section className="itc-inverse">
|
||||
<Container>
|
||||
<div className="itc-testimonials">
|
||||
<h2
|
||||
className="itc-display-md itc-testimonials-title"
|
||||
style={{ margin: "0 auto 32px" }}
|
||||
>
|
||||
Trusted by customers all over the world
|
||||
</h2>
|
||||
<div className="itc-quote-grid">
|
||||
{TESTIMONIALS.map((t) => (
|
||||
<figure className="itc-quote-card" key={t.name}>
|
||||
<div className="itc-quote-mark" aria-hidden="true">
|
||||
“
|
||||
</div>
|
||||
<blockquote className="itc-quote-text">{t.quote}</blockquote>
|
||||
<figcaption className="itc-quote-attr">
|
||||
<span className="itc-quote-avatar" aria-hidden="true">
|
||||
{t.avatarSrc ? (
|
||||
<img
|
||||
src={t.avatarSrc}
|
||||
alt=""
|
||||
className="itc-quote-avatar-img"
|
||||
/>
|
||||
) : (
|
||||
t.initial
|
||||
)}
|
||||
</span>
|
||||
<span className="itc-quote-meta">
|
||||
<span className="itc-quote-name">{t.name}</span>
|
||||
<a
|
||||
href={t.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="itc-quote-handle"
|
||||
>
|
||||
{t.network === "x" ? (
|
||||
<IconX size={11} />
|
||||
) : t.network === "linkedin" ? (
|
||||
<IconLinkedIn size={12} />
|
||||
) : (
|
||||
<IconLink size={12} />
|
||||
)}
|
||||
{t.handle}
|
||||
</a>
|
||||
</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Product ─────────────────────────────────────────────────────────
|
||||
|
||||
const FEATURE_CARDS = [
|
||||
{
|
||||
page: featurePages.keywordResearch,
|
||||
blurb: "Find ideas, demand, difficulty, intent, and live SERPs.",
|
||||
},
|
||||
{
|
||||
page: featurePages.domainOverview,
|
||||
blurb: "Estimate organic traffic and ranking keywords.",
|
||||
},
|
||||
{
|
||||
page: featurePages.backlinkChecker,
|
||||
blurb: "Inspect backlinks, referring domains, and link quality.",
|
||||
},
|
||||
{
|
||||
page: featurePages.rankTracking,
|
||||
blurb: "Track keyword positions over time.",
|
||||
},
|
||||
{
|
||||
page: featurePages.siteAudit,
|
||||
blurb: "Crawl pages and surface technical issues.",
|
||||
},
|
||||
{
|
||||
page: featurePages.aiBrandVisibility,
|
||||
blurb: "Review AI mentions, citations, and prompts.",
|
||||
},
|
||||
{
|
||||
page: featurePages.aiSearchPrompts,
|
||||
blurb: "Compare prompts across supported AI models.",
|
||||
},
|
||||
{
|
||||
page: featurePages.savedKeywords,
|
||||
blurb: "Organize ideas for content and tracking.",
|
||||
},
|
||||
];
|
||||
|
||||
function DemoVideo() {
|
||||
return (
|
||||
<video
|
||||
style={{ width: "100%" }}
|
||||
width={1280}
|
||||
height={966}
|
||||
poster="/demo-poster.webp"
|
||||
muted
|
||||
loop
|
||||
autoPlay
|
||||
playsInline
|
||||
preload="metadata"
|
||||
aria-label="OpenSEO product demo: running keyword research"
|
||||
>
|
||||
<source src="/demo.mp4" type="video/mp4" />
|
||||
<img
|
||||
src="/demo-poster.webp"
|
||||
alt="OpenSEO keyword research dashboard"
|
||||
width={1280}
|
||||
height={966}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</video>
|
||||
);
|
||||
}
|
||||
|
||||
function ProductSection() {
|
||||
return (
|
||||
<section className="itc-section itc-section-demo">
|
||||
<Container>
|
||||
<div className="itc-narrow">
|
||||
<h2 className="itc-display-lg">See OpenSEO in action</h2>
|
||||
<p className="itc-subhead itc-muted" style={{ margin: "20px 0 0" }}>
|
||||
Keyword research, competitor analysis, backlinks, rank tracking,
|
||||
technical audits, and AI-search visibility, all on real DataForSEO
|
||||
data and connected to each other.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="itc-mockup" style={{ marginTop: 48 }}>
|
||||
<div className="itc-mockup-media" style={{ aspectRatio: "1280/966" }}>
|
||||
<DemoVideo />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="itc-feature-list-grid">
|
||||
{FEATURE_CARDS.map(({ page, blurb }) => (
|
||||
<a
|
||||
key={page.slug}
|
||||
href={`/features/${page.slug}`}
|
||||
className="itc-card itc-feature-list-card"
|
||||
>
|
||||
<p style={{ margin: 0, fontSize: 16, fontWeight: 500 }}>
|
||||
{page.eyebrow}
|
||||
</p>
|
||||
<p className="itc-body-sm itc-muted" style={{ margin: "8px 0 0" }}>
|
||||
{blurb}
|
||||
</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="itc-feature-more-header">
|
||||
<a href="/features" className="itc-textlink">
|
||||
All features <IconArrowRight size={15} className="itc-arrow" />
|
||||
</a>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── MCP: the page's one Fin Orange moment ───────────────────────────
|
||||
|
||||
type McpClient = {
|
||||
name: string;
|
||||
Icon: (props: SVGProps<SVGSVGElement>) => ReactNode;
|
||||
};
|
||||
|
||||
const MCP_CLIENTS: McpClient[] = [
|
||||
{ name: "Claude", Icon: ClaudeIcon },
|
||||
{ name: "Codex", Icon: CodexIcon },
|
||||
{ name: "OpenClaw", Icon: OpenClawIcon },
|
||||
{ name: "OpenCode", Icon: OpenCodeIcon },
|
||||
{ name: "Gemini", Icon: GeminiIcon },
|
||||
];
|
||||
|
||||
function McpSection() {
|
||||
return (
|
||||
<section className="itc-mcp-section">
|
||||
<Container>
|
||||
<div className="itc-mcp-grid">
|
||||
<div>
|
||||
<p className="itc-eyebrow" style={{ color: "#ff5600" }}>
|
||||
Model Context Protocol
|
||||
</p>
|
||||
<h2 className="itc-display-lg">Get superpowers with the MCP</h2>
|
||||
<p className="itc-body-lg itc-muted" style={{ margin: "20px 0 0" }}>
|
||||
Give your agent real SEO data instead of guesses. It can research
|
||||
keywords, competitors, backlinks, and Google Search Console
|
||||
performance, then you can review the work in OpenSEO.
|
||||
</p>
|
||||
<div className="itc-agent-icons">
|
||||
{MCP_CLIENTS.map(({ name, Icon }) => (
|
||||
<span key={name} className="itc-agent-icon" title={name}>
|
||||
<Icon aria-hidden="true" />
|
||||
<span
|
||||
style={{
|
||||
clip: "rect(0 0 0 0)",
|
||||
clipPath: "inset(50%)",
|
||||
height: 1,
|
||||
overflow: "hidden",
|
||||
position: "absolute",
|
||||
whiteSpace: "nowrap",
|
||||
width: 1,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<a href="/docs/mcp" className="itc-btn itc-btn-fin">
|
||||
Learn about MCP tools
|
||||
<IconArrowRight size={16} className="itc-arrow" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="itc-terminal">
|
||||
<div className="itc-terminal-bar">
|
||||
<span style={{ display: "flex", gap: 6 }} aria-hidden="true">
|
||||
<span className="itc-terminal-dot" />
|
||||
<span className="itc-terminal-dot" />
|
||||
<span className="itc-terminal-dot" />
|
||||
</span>
|
||||
<span className="itc-terminal-label">claude · openseo mcp</span>
|
||||
</div>
|
||||
<pre>
|
||||
<code>
|
||||
<span className="t-orange">›</span> find and cluster keywords
|
||||
for{" "}
|
||||
<span className="t-bright">openseo.so</span>
|
||||
{"\n\n"}
|
||||
<span className="t-dim">
|
||||
⏺ openseo.keyword_research(seed: "open source seo")
|
||||
</span>
|
||||
{"\n"}
|
||||
{" "}keyword{" "}volume{" "}kd{"\n"}
|
||||
{" "}open source seo{" "}
|
||||
<span className="t-bright">1,300</span>
|
||||
{" "}
|
||||
<span className="t-dim">12</span>
|
||||
{"\n"}
|
||||
{" "}open source seo tools{" "}
|
||||
<span className="t-bright">720</span>
|
||||
{" "}
|
||||
<span className="t-dim">9</span>
|
||||
{"\n"}
|
||||
{" "}self-hosted seo platform{" "}
|
||||
<span className="t-bright">210</span>
|
||||
{" "}
|
||||
<span className="t-dim">4</span>
|
||||
{"\n\n"}
|
||||
<span className="t-orange">✓</span>
|
||||
<span className="t-dim">
|
||||
{" "}
|
||||
Saved 3 keywords to your workspace.
|
||||
</span>
|
||||
{"\n"}
|
||||
<span className="t-orange">↳</span>
|
||||
<span className="t-dim"> View data in app: </span>
|
||||
<span className="t-bright">app.openseo.so/keywords</span>
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ClaudeIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
viewBox="0 0 256 257"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#D97757"
|
||||
d="m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CodexIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
fill="#111"
|
||||
fillRule="evenodd"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function OpenClawIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 120 120"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="intercom-openclaw-lobster-gradient"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop offset="0%" stopColor="#ff4d4d" />
|
||||
<stop offset="100%" stopColor="#991b1b" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="M60 10 C30 10 15 35 15 55 C15 75 30 95 45 100 L45 110 L55 110 L55 100 C55 100 60 102 65 100 L65 110 L75 110 L75 100 C90 95 105 75 105 55 C105 35 90 10 60 10Z"
|
||||
fill="url(#intercom-openclaw-lobster-gradient)"
|
||||
/>
|
||||
<path
|
||||
d="M20 45 C5 40 0 50 5 60 C10 70 20 65 25 55 C28 48 25 45 20 45Z"
|
||||
fill="url(#intercom-openclaw-lobster-gradient)"
|
||||
/>
|
||||
<path
|
||||
d="M100 45 C115 40 120 50 115 60 C110 70 100 65 95 55 C92 48 95 45 100 45Z"
|
||||
fill="url(#intercom-openclaw-lobster-gradient)"
|
||||
/>
|
||||
<path
|
||||
d="M45 15 Q35 5 30 8"
|
||||
stroke="#ff4d4d"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="3"
|
||||
/>
|
||||
<path
|
||||
d="M75 15 Q85 5 90 8"
|
||||
stroke="#ff4d4d"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="3"
|
||||
/>
|
||||
<circle cx="45" cy="35" r="6" fill="#050810" />
|
||||
<circle cx="75" cy="35" r="6" fill="#050810" />
|
||||
<circle cx="46" cy="34" r="2.5" fill="#00e5cc" />
|
||||
<circle cx="76" cy="34" r="2.5" fill="#00e5cc" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function OpenCodeIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 512 512"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<rect width="512" height="512" fill="#FDFCFC" />
|
||||
<path d="M320 224V352H192V224H320Z" fill="#E6E5E6" />
|
||||
<path
|
||||
fill="#17181C"
|
||||
fillRule="evenodd"
|
||||
d="M384 416H128V96H384V416ZM320 160H192V352H320V160Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function GeminiIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 296 298"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
id="intercom-gemini-a"
|
||||
width="296"
|
||||
height="298"
|
||||
x="0"
|
||||
y="0"
|
||||
maskUnits="userSpaceOnUse"
|
||||
style={{ maskType: "alpha" }}
|
||||
>
|
||||
<path
|
||||
fill="#3186FF"
|
||||
d="M141.201 4.886c2.282-6.17 11.042-6.071 13.184.148l5.985 17.37a184.004 184.004 0 0 0 111.257 113.049l19.304 6.997c6.143 2.227 6.156 10.91.02 13.155l-19.35 7.082a184.001 184.001 0 0 0-109.495 109.385l-7.573 20.629c-2.241 6.105-10.869 6.121-13.133.025l-7.908-21.296a184 184 0 0 0-109.02-108.658l-19.698-7.239c-6.102-2.243-6.118-10.867-.025-13.132l20.083-7.467A183.998 183.998 0 0 0 133.291 26.28l7.91-21.394Z"
|
||||
/>
|
||||
</mask>
|
||||
<g mask="url(#intercom-gemini-a)">
|
||||
<g filter="url(#intercom-gemini-b)">
|
||||
<ellipse cx="163" cy="149" fill="#3689FF" rx="196" ry="159" />
|
||||
</g>
|
||||
<g filter="url(#intercom-gemini-c)">
|
||||
<ellipse
|
||||
cx="33.5"
|
||||
cy="142.5"
|
||||
fill="#F6C013"
|
||||
rx="68.5"
|
||||
ry="72.5"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#intercom-gemini-d)">
|
||||
<ellipse
|
||||
cx="19.5"
|
||||
cy="148.5"
|
||||
fill="#F6C013"
|
||||
rx="68.5"
|
||||
ry="72.5"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#intercom-gemini-e)">
|
||||
<path
|
||||
fill="#FA4340"
|
||||
d="M194 10.5C172 82.5 65.5 134.333 22.5 135L144-66l50 76.5Z"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#intercom-gemini-f)">
|
||||
<path
|
||||
fill="#FA4340"
|
||||
d="M190.5-12.5C168.5 59.5 62 111.333 19 112L140.5-89l50 76.5Z"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#intercom-gemini-g)">
|
||||
<path
|
||||
fill="#14BB69"
|
||||
d="M194.5 279.5C172.5 207.5 66 155.667 23 155l121.5 201 50-76.5Z"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#intercom-gemini-h)">
|
||||
<path
|
||||
fill="#14BB69"
|
||||
d="M196.5 320.5C174.5 248.5 68 196.667 25 196l121.5 201 50-76.5Z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="intercom-gemini-b"
|
||||
width="464"
|
||||
height="390"
|
||||
x="-69"
|
||||
y="-46"
|
||||
colorInterpolationFilters="sRGB"
|
||||
filterUnits="userSpaceOnUse"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feGaussianBlur
|
||||
result="effect1_foregroundBlur_69_17998"
|
||||
stdDeviation="18"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="intercom-gemini-c"
|
||||
width="265"
|
||||
height="273"
|
||||
x="-99"
|
||||
y="6"
|
||||
colorInterpolationFilters="sRGB"
|
||||
filterUnits="userSpaceOnUse"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feGaussianBlur
|
||||
result="effect1_foregroundBlur_69_17998"
|
||||
stdDeviation="32"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="intercom-gemini-d"
|
||||
width="265"
|
||||
height="273"
|
||||
x="-113"
|
||||
y="12"
|
||||
colorInterpolationFilters="sRGB"
|
||||
filterUnits="userSpaceOnUse"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feGaussianBlur
|
||||
result="effect1_foregroundBlur_69_17998"
|
||||
stdDeviation="32"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="intercom-gemini-e"
|
||||
width="299.5"
|
||||
height="329"
|
||||
x="-41.5"
|
||||
y="-130"
|
||||
colorInterpolationFilters="sRGB"
|
||||
filterUnits="userSpaceOnUse"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feGaussianBlur
|
||||
result="effect1_foregroundBlur_69_17998"
|
||||
stdDeviation="32"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="intercom-gemini-f"
|
||||
width="299.5"
|
||||
height="329"
|
||||
x="-45"
|
||||
y="-153"
|
||||
colorInterpolationFilters="sRGB"
|
||||
filterUnits="userSpaceOnUse"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feGaussianBlur
|
||||
result="effect1_foregroundBlur_69_17998"
|
||||
stdDeviation="32"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="intercom-gemini-g"
|
||||
width="299.5"
|
||||
height="329"
|
||||
x="-41"
|
||||
y="91"
|
||||
colorInterpolationFilters="sRGB"
|
||||
filterUnits="userSpaceOnUse"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feGaussianBlur
|
||||
result="effect1_foregroundBlur_69_17998"
|
||||
stdDeviation="32"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="intercom-gemini-h"
|
||||
width="299.5"
|
||||
height="329"
|
||||
x="-39"
|
||||
y="132"
|
||||
colorInterpolationFilters="sRGB"
|
||||
filterUnits="userSpaceOnUse"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feGaussianBlur
|
||||
result="effect1_foregroundBlur_69_17998"
|
||||
stdDeviation="32"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Open source ─────────────────────────────────────────────────────
|
||||
|
||||
function OpenSourceSection() {
|
||||
return (
|
||||
<section className="itc-section itc-section-open-source">
|
||||
<Container>
|
||||
<div className="itc-narrow">
|
||||
<h2 className="itc-display-lg">100% open source</h2>
|
||||
<p className="itc-subhead itc-muted" style={{ margin: "20px 0 0" }}>
|
||||
People should have the option to self-host and customize their
|
||||
tools. If you ever hear someone talking about building their own
|
||||
tool from scratch, tell them to build on top of OpenSEO.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
gap: 24,
|
||||
marginTop: 32,
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={GITHUB_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="itc-btn itc-btn-secondary"
|
||||
>
|
||||
<IconGithub size={16} />
|
||||
Star on GitHub
|
||||
</a>
|
||||
<a href="/open-source-seo" className="itc-textlink">
|
||||
Why Open Source?{" "}
|
||||
<IconArrowRight size={15} className="itc-arrow" />
|
||||
</a>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Footer ──────────────────────────────────────────────────────────
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
<footer className="itc-footer">
|
||||
<Container>
|
||||
<div
|
||||
style={{
|
||||
paddingTop: 64,
|
||||
paddingBottom: 40,
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 32,
|
||||
alignItems: "flex-end",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: "1px solid #ebe7e1",
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: 420 }}>
|
||||
<p style={{ margin: 0, fontSize: 15, fontWeight: 500 }}>
|
||||
Stay in the loop
|
||||
</p>
|
||||
<p className="itc-body-sm itc-muted" style={{ margin: "6px 0 0" }}>
|
||||
Product updates, new features, and the occasional
|
||||
behind-the-scenes.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="itc-newsletter"
|
||||
style={{ width: "100%", maxWidth: 384 }}
|
||||
>
|
||||
<NewsletterSignup />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="itc-sitefooter" style={{ paddingTop: 40 }}>
|
||||
<SiteFooter />
|
||||
</div>
|
||||
|
||||
<p
|
||||
className="itc-caption itc-subtle"
|
||||
style={{ margin: 0, padding: "40px 0 32px" }}
|
||||
>
|
||||
© 2026 Every App, Inc.
|
||||
</p>
|
||||
</Container>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Page ────────────────────────────────────────────────────────────
|
||||
|
||||
export function LandingPage() {
|
||||
return (
|
||||
<div className="itc">
|
||||
<Hero />
|
||||
<McpSection />
|
||||
<Testimonial />
|
||||
<OpenSourceSection />
|
||||
<ProductSection />
|
||||
<Footer />
|
||||
<a
|
||||
href={DISCORD_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="itc-discord"
|
||||
aria-label="Join the OpenSEO Discord"
|
||||
>
|
||||
<IconDiscord size={18} />
|
||||
<span>Discord</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -13,17 +13,23 @@ type LegalPageProps = {
|
||||
export function LegalPage({ title, description, children }: LegalPageProps) {
|
||||
return (
|
||||
<HomeLayout {...baseOptions()}>
|
||||
<article className="max-w-3xl mx-auto px-6 py-12 md:py-24">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-4xl font-bold mb-4">{title}</h1>
|
||||
<article className="mx-auto max-w-3xl bg-[var(--color-surface)] px-6 py-12 text-neutral-950 md:py-24">
|
||||
<header className="mb-10 border-b border-[var(--color-border-subtle)] pb-8">
|
||||
<h1 className="mb-4 text-4xl font-semibold tracking-tight md:text-6xl">
|
||||
{title}
|
||||
</h1>
|
||||
{description ? (
|
||||
<p className="text-lg text-fd-muted-foreground">{description}</p>
|
||||
<p className="text-lg leading-8 text-[var(--color-brand-muted)]">
|
||||
{description}
|
||||
</p>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<DocsBody>{children}</DocsBody>
|
||||
<DocsBody className="text-neutral-800 [&_a]:!text-neutral-950 [&_a]:underline [&_a]:decoration-[var(--color-brand-accent)] [&_a]:underline-offset-4 [&_h2]:text-neutral-950 [&_h3]:text-neutral-950 [&_li]:text-neutral-700 [&_p]:text-neutral-700 [&_strong]:text-neutral-950">
|
||||
{children}
|
||||
</DocsBody>
|
||||
|
||||
<div className="mt-12 border-t border-neutral-200 pt-8">
|
||||
<div className="mt-16 border-t border-[var(--color-border-subtle)] pt-8">
|
||||
<SiteFooter className="text-xs text-neutral-600 [&_a]:transition-colors [&_a]:hover:text-neutral-900" />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
72
web/src/components/newsletter-signup.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { type FormEvent, useState } from "react";
|
||||
|
||||
/** Newsletter subscribe form. Shared by the marketing footer and home page. */
|
||||
export function NewsletterSignup() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [status, setStatus] = useState<
|
||||
"idle" | "loading" | "success" | "error"
|
||||
>("idle");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus("loading");
|
||||
setErrorMessage("");
|
||||
try {
|
||||
const res = await fetch("/api/subscribe", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
(data as { error?: string }).error || "Something went wrong",
|
||||
);
|
||||
}
|
||||
setStatus("success");
|
||||
setEmail("");
|
||||
} catch (err) {
|
||||
setStatus("error");
|
||||
setErrorMessage(
|
||||
err instanceof Error ? err.message : "Something went wrong",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (status === "success") {
|
||||
return <p className="text-sm text-neutral-900">You're on the list.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} autoComplete="on">
|
||||
<div className="flex gap-2">
|
||||
<label htmlFor="newsletter-email" className="sr-only">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="newsletter-email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="you@example.com"
|
||||
disabled={status === "loading"}
|
||||
className="h-10 min-w-0 flex-1 rounded-lg border border-[var(--color-border-subtle)] bg-white px-3 text-sm text-neutral-900 placeholder:text-neutral-500 transition focus:border-neutral-900 focus:outline-none focus:ring-1 focus:ring-neutral-900"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === "loading"}
|
||||
className="h-10 shrink-0 rounded-lg bg-neutral-950 px-5 text-sm font-medium text-white transition-colors hover:bg-neutral-800 disabled:opacity-50"
|
||||
>
|
||||
{status === "loading" ? "..." : "Subscribe"}
|
||||
</button>
|
||||
</div>
|
||||
{status === "error" && (
|
||||
<p className="mt-2 text-xs text-red-600">{errorMessage}</p>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@ -18,6 +18,7 @@ export type FeaturePage = {
|
||||
label: string;
|
||||
value: string;
|
||||
}>;
|
||||
showMetrics?: boolean;
|
||||
useCases: string[];
|
||||
differentiators: string[];
|
||||
related: Array<{
|
||||
@ -70,6 +71,7 @@ export const featurePages = {
|
||||
{ label: "CPC", value: "Commercial signal" },
|
||||
{ label: "SERP results", value: "Search context" },
|
||||
],
|
||||
showMetrics: true,
|
||||
useCases: [
|
||||
"Build a content roadmap from real keyword data.",
|
||||
"Find lower-competition variants before writing.",
|
||||
@ -145,6 +147,7 @@ export const featurePages = {
|
||||
{ label: "Affected pages", value: "Scope" },
|
||||
{ label: "Audit history", value: "Progress" },
|
||||
],
|
||||
showMetrics: true,
|
||||
useCases: [
|
||||
"Audit a new site before publishing SEO work.",
|
||||
"Find technical issues after a migration or redesign.",
|
||||
@ -217,6 +220,7 @@ export const featurePages = {
|
||||
{ label: "Target URLs", value: "Distribution" },
|
||||
{ label: "Rank and spam signals", value: "Quality context" },
|
||||
],
|
||||
showMetrics: true,
|
||||
useCases: [
|
||||
"See who links to a competitor.",
|
||||
"Inspect link opportunities for important pages.",
|
||||
@ -292,6 +296,7 @@ export const featurePages = {
|
||||
{ label: "Top keywords", value: "Rankings" },
|
||||
{ label: "Top pages", value: "Organic reach" },
|
||||
],
|
||||
showMetrics: true,
|
||||
useCases: [
|
||||
"Research a competitor before writing a content plan.",
|
||||
"Estimate a site's organic footprint.",
|
||||
@ -372,6 +377,7 @@ export const featurePages = {
|
||||
{ label: "SERP features", value: "Context" },
|
||||
{ label: "Position change", value: "Movement" },
|
||||
],
|
||||
showMetrics: true,
|
||||
useCases: [
|
||||
"Monitor target keywords after publishing content.",
|
||||
"Track launch, migration, and optimization impact.",
|
||||
|
||||
@ -15,7 +15,7 @@ export function baseOptions(): BaseLayoutProps {
|
||||
items: [
|
||||
{
|
||||
text: "Blog",
|
||||
description: "Founder-focused SEO articles.",
|
||||
description: "SEO articles and guides.",
|
||||
url: "/blogs",
|
||||
},
|
||||
{
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
import { createFileRoute, Link, Outlet } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
createFileRoute,
|
||||
Link,
|
||||
Outlet,
|
||||
useLocation,
|
||||
} from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NewsletterSignup } from "@/components/newsletter-signup";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { featureGroups } from "@/lib/feature-pages";
|
||||
|
||||
@ -84,17 +90,31 @@ export const Route = createFileRoute("/_marketing")({
|
||||
|
||||
function MarketingLayout() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const { pathname } = useLocation();
|
||||
// The home route owns the full viewport width (and its own footer/CTA band);
|
||||
// every other marketing page gets the shared marketing canvas and footer.
|
||||
const isHome = pathname === "/";
|
||||
|
||||
// On the landing route, paint html/body cream so the area behind the
|
||||
// floating nav and any overscroll matches the landing canvas.
|
||||
useEffect(() => {
|
||||
if (!isHome) return;
|
||||
const root = document.documentElement;
|
||||
const prevRoot = root.style.backgroundColor;
|
||||
const prevBody = document.body.style.backgroundColor;
|
||||
root.style.backgroundColor = "#f5f1ec";
|
||||
document.body.style.backgroundColor = "#f5f1ec";
|
||||
return () => {
|
||||
root.style.backgroundColor = prevRoot;
|
||||
document.body.style.backgroundColor = prevBody;
|
||||
};
|
||||
}, [isHome]);
|
||||
|
||||
return (
|
||||
<main
|
||||
className="bg-white text-neutral-900 min-h-screen"
|
||||
style={{
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
||||
}}
|
||||
>
|
||||
<div className="mx-auto w-full max-w-6xl px-4 pt-6 sm:px-6 md:pt-8">
|
||||
<main className="min-h-screen bg-[var(--color-surface)] text-[var(--color-brand)]">
|
||||
<div className="relative z-50 mx-auto w-full max-w-6xl px-4 pt-6 sm:px-6 md:pt-8">
|
||||
<div className="relative mx-auto max-w-5xl">
|
||||
<nav className="grid min-h-14 grid-cols-[1fr_auto] items-center gap-3 rounded-full border border-neutral-200 bg-white/95 px-4 py-2.5 shadow-sm shadow-neutral-900/5 md:grid-cols-[1fr_auto_1fr] md:px-5">
|
||||
<nav className="grid min-h-14 grid-cols-[1fr_auto] items-center gap-3 rounded-full border border-[var(--color-border-subtle)] bg-white/90 px-4 py-2.5 shadow-sm shadow-neutral-900/5 backdrop-blur md:grid-cols-[1fr_auto_1fr] md:px-5">
|
||||
<Link
|
||||
to="/"
|
||||
className="text-sm font-semibold hover:opacity-80 transition-opacity"
|
||||
@ -119,7 +139,7 @@ function MarketingLayout() {
|
||||
aria-label={mobileMenuOpen ? "Close menu" : "Open menu"}
|
||||
aria-expanded={mobileMenuOpen}
|
||||
onClick={() => setMobileMenuOpen((open) => !open)}
|
||||
className="inline-flex h-9 w-9 items-center justify-center rounded-full text-neutral-900 transition-colors hover:bg-neutral-100 md:hidden"
|
||||
className="inline-flex h-9 w-9 items-center justify-center rounded-full text-neutral-900 transition-colors hover:bg-[#f5f1ec] md:hidden"
|
||||
>
|
||||
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}
|
||||
</button>
|
||||
@ -136,7 +156,7 @@ function MarketingLayout() {
|
||||
</a>
|
||||
<a
|
||||
href="https://app.openseo.so/sign-in"
|
||||
className="hidden h-9 items-center rounded-full border border-neutral-300 px-4 text-sm font-medium text-neutral-900 transition-colors hover:border-neutral-900 md:inline-flex"
|
||||
className="hidden h-9 items-center rounded-full border border-[var(--color-border-subtle)] px-4 text-sm font-medium text-neutral-900 transition-colors hover:border-neutral-900 md:inline-flex"
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
@ -144,7 +164,7 @@ function MarketingLayout() {
|
||||
</nav>
|
||||
|
||||
{mobileMenuOpen ? (
|
||||
<div className="absolute left-0 right-0 top-full z-30 mt-3 rounded-2xl border border-neutral-200 bg-white p-3 shadow-xl shadow-neutral-900/10 md:hidden">
|
||||
<div className="absolute left-0 right-0 top-full z-30 mt-3 rounded-2xl border border-[var(--color-border-subtle)] bg-white p-3 shadow-xl shadow-neutral-900/10 md:hidden">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<a
|
||||
href="https://app.openseo.so/sign-in"
|
||||
@ -156,7 +176,7 @@ function MarketingLayout() {
|
||||
<a
|
||||
href="https://app.openseo.so/sign-in"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex h-11 items-center justify-center rounded-xl border border-neutral-200 px-3 text-sm font-semibold text-neutral-800 transition-colors hover:border-neutral-300 hover:bg-neutral-50"
|
||||
className="flex h-11 items-center justify-center rounded-xl border border-[var(--color-border-subtle)] px-3 text-sm font-semibold text-neutral-800 transition-colors hover:border-neutral-900 hover:bg-[#f5f1ec]"
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
@ -188,10 +208,14 @@ function MarketingLayout() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl mx-auto px-6 py-16 md:py-24">
|
||||
{isHome ? (
|
||||
<Outlet />
|
||||
) : (
|
||||
<div className="mx-auto max-w-5xl px-6 py-16 md:py-24">
|
||||
<Outlet />
|
||||
<MarketingFooter />
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -201,7 +225,7 @@ function ResourcesDropdown() {
|
||||
{
|
||||
label: "Blog",
|
||||
href: "/blogs",
|
||||
description: "Founder-focused SEO articles.",
|
||||
description: "SEO articles and guides.",
|
||||
},
|
||||
{
|
||||
label: "Why Open Source?",
|
||||
@ -221,7 +245,7 @@ function ResourcesDropdown() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<div className="group relative">
|
||||
<a
|
||||
href="/blogs"
|
||||
className="text-sm font-semibold text-neutral-600 transition-colors hover:text-neutral-900 md:hidden"
|
||||
@ -230,17 +254,17 @@ function ResourcesDropdown() {
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="hidden text-sm font-semibold text-neutral-600 transition-colors hover:text-neutral-900 md:inline-flex"
|
||||
className="hidden h-10 items-center text-sm font-semibold text-neutral-600 transition-colors hover:text-neutral-900 md:inline-flex"
|
||||
>
|
||||
Resources
|
||||
</button>
|
||||
<div className="pointer-events-none absolute left-1/2 top-full z-20 hidden w-[260px] -translate-x-1/2 pt-4 opacity-0 transition md:block group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100">
|
||||
<div className="rounded-lg border border-neutral-200 bg-white p-3 shadow-xl shadow-neutral-900/10">
|
||||
<div className="pointer-events-none absolute left-1/2 top-[calc(100%-2px)] z-20 hidden w-[280px] -translate-x-1/2 pt-2 opacity-0 transition md:block group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100">
|
||||
<div className="rounded-lg border border-[var(--color-border-subtle)] bg-white p-3 shadow-xl shadow-neutral-900/10">
|
||||
{resources.map((resource) => (
|
||||
<a
|
||||
key={resource.href}
|
||||
href={resource.href}
|
||||
className="block rounded-md px-3 py-2.5 transition-colors hover:bg-neutral-50"
|
||||
className="block rounded-md px-3 py-2.5 transition-colors hover:bg-[#f5f1ec]"
|
||||
>
|
||||
<span className="block text-sm font-semibold text-neutral-900">
|
||||
{resource.label}
|
||||
@ -258,7 +282,7 @@ function ResourcesDropdown() {
|
||||
|
||||
function FeatureDropdown() {
|
||||
return (
|
||||
<div className="relative group">
|
||||
<div className="group relative">
|
||||
<Link
|
||||
to="/features"
|
||||
className="text-sm font-semibold text-neutral-600 transition-colors hover:text-neutral-900 md:hidden"
|
||||
@ -267,12 +291,12 @@ function FeatureDropdown() {
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="hidden text-sm font-semibold text-neutral-600 transition-colors hover:text-neutral-900 md:inline-flex"
|
||||
className="hidden h-10 items-center text-sm font-semibold text-neutral-600 transition-colors hover:text-neutral-900 md:inline-flex"
|
||||
>
|
||||
Features
|
||||
</button>
|
||||
<div className="pointer-events-none absolute left-1/2 top-full z-20 hidden w-[560px] -translate-x-1/2 pt-4 opacity-0 transition md:block group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100">
|
||||
<div className="rounded-lg border border-neutral-200 bg-white p-5 shadow-xl shadow-neutral-900/10">
|
||||
<div className="pointer-events-none absolute left-1/2 top-[calc(100%-2px)] z-20 hidden w-[560px] -translate-x-1/2 pt-2 opacity-0 transition md:block group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100">
|
||||
<div className="rounded-lg border border-[var(--color-border-subtle)] bg-white p-5 shadow-xl shadow-neutral-900/10">
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-6">
|
||||
{featureGroups.map((group) => (
|
||||
<div key={group.label}>
|
||||
@ -284,7 +308,7 @@ function FeatureDropdown() {
|
||||
<a
|
||||
key={page.slug}
|
||||
href={`/features/${page.slug}`}
|
||||
className="block rounded-md px-2 py-1.5 transition-colors hover:bg-neutral-50"
|
||||
className="block rounded-md px-2 py-1.5 transition-colors hover:bg-[#f5f1ec]"
|
||||
>
|
||||
<span className="block text-sm font-semibold text-neutral-900">
|
||||
{page.eyebrow}
|
||||
@ -304,7 +328,7 @@ function FeatureDropdown() {
|
||||
<div className="mt-3 space-y-2">
|
||||
<a
|
||||
href="/features/mcp"
|
||||
className="block rounded-md p-2 transition-colors hover:bg-neutral-50"
|
||||
className="block rounded-md p-2 transition-colors hover:bg-[#f5f1ec]"
|
||||
>
|
||||
<span className="text-sm font-semibold text-neutral-900">
|
||||
OpenSEO MCP
|
||||
@ -315,18 +339,18 @@ function FeatureDropdown() {
|
||||
</a>
|
||||
<a
|
||||
href="/google-search-console-mcp"
|
||||
className="block rounded-md p-2 transition-colors hover:bg-neutral-50"
|
||||
className="block rounded-md p-2 transition-colors hover:bg-[#f5f1ec]"
|
||||
>
|
||||
<span className="text-sm font-semibold text-neutral-900">
|
||||
Search Console MCP
|
||||
</span>
|
||||
<span className="mt-0.5 block text-xs leading-relaxed text-neutral-600">
|
||||
Free first-party GSC data for agents.
|
||||
Search Console data for agents.
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="/features"
|
||||
className="block rounded-md border border-neutral-200 bg-neutral-50 px-2 py-1.5 text-sm font-medium text-neutral-900 transition-colors hover:border-neutral-900"
|
||||
className="block rounded-md border border-[var(--color-border-subtle)] bg-[#f5f1ec] px-2 py-1.5 text-sm font-medium text-neutral-900 transition-colors hover:border-neutral-900"
|
||||
>
|
||||
View all features <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
@ -339,49 +363,11 @@ function FeatureDropdown() {
|
||||
);
|
||||
}
|
||||
|
||||
function useNewsletter() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [status, setStatus] = useState<
|
||||
"idle" | "loading" | "success" | "error"
|
||||
>("idle");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus("loading");
|
||||
setErrorMessage("");
|
||||
try {
|
||||
const res = await fetch("/api/subscribe", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
(data as { error?: string }).error || "Something went wrong",
|
||||
);
|
||||
}
|
||||
setStatus("success");
|
||||
setEmail("");
|
||||
} catch (err) {
|
||||
setStatus("error");
|
||||
setErrorMessage(
|
||||
err instanceof Error ? err.message : "Something went wrong",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return { email, setEmail, status, errorMessage, handleSubmit };
|
||||
}
|
||||
|
||||
function MarketingFooter() {
|
||||
const nl = useNewsletter();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Newsletter */}
|
||||
<div className="mt-12 pt-8 border-t border-neutral-200">
|
||||
<div className="mt-16 border-t border-[var(--color-border-subtle)] pt-8">
|
||||
<p className="text-sm font-semibold text-neutral-900">
|
||||
Stay in the loop
|
||||
</p>
|
||||
@ -389,42 +375,13 @@ function MarketingFooter() {
|
||||
Product updates, new features, and the occasional behind-the-scenes.
|
||||
</p>
|
||||
<div className="mt-3">
|
||||
{nl.status === "success" ? (
|
||||
<p className="text-sm text-neutral-900">You're on the list.</p>
|
||||
) : (
|
||||
<form onSubmit={nl.handleSubmit} autoComplete="on">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
id="newsletter-email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
value={nl.email}
|
||||
onChange={(e) => nl.setEmail(e.target.value)}
|
||||
placeholder="you@example.com"
|
||||
className="flex-1 min-w-0 h-10 px-3 text-sm border border-neutral-300 rounded-md bg-white placeholder:text-neutral-500 focus:outline-none focus:ring-1 focus:ring-neutral-900 transition-all"
|
||||
disabled={nl.status === "loading"}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={nl.status === "loading"}
|
||||
className="h-10 px-5 text-sm font-medium bg-neutral-900 text-white rounded-md hover:bg-neutral-800 transition-colors disabled:opacity-50 shrink-0"
|
||||
>
|
||||
{nl.status === "loading" ? "..." : "Subscribe"}
|
||||
</button>
|
||||
</div>
|
||||
{nl.status === "error" && (
|
||||
<p className="text-red-600 text-xs mt-2">{nl.errorMessage}</p>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
<NewsletterSignup />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-8">
|
||||
<SiteFooter className="text-xs text-neutral-600 [&_a]:hover:text-neutral-900 [&_a]:transition-colors" />
|
||||
<SiteFooter className="text-xs text-neutral-600 [&_a]:transition-colors [&_a]:hover:text-neutral-900" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import type { ReactNode } from "react";
|
||||
import { featureGroups } from "@/lib/feature-pages";
|
||||
import { buildPageSeo } from "@/lib/seo";
|
||||
|
||||
@ -18,105 +19,111 @@ export const Route = createFileRoute("/_marketing/features/")({
|
||||
|
||||
function FeaturesIndex() {
|
||||
return (
|
||||
<>
|
||||
<p className="text-sm font-medium text-neutral-500">
|
||||
<article className="mx-auto max-w-5xl">
|
||||
<p className="text-sm font-medium text-[var(--color-brand-accent)]">
|
||||
Open-source SEO tools
|
||||
</p>
|
||||
<h1 className="mt-3 text-3xl font-bold leading-tight tracking-tight">
|
||||
A delightful suite of SEO tools
|
||||
<h1 className="mt-3 max-w-3xl text-4xl font-semibold leading-tight tracking-tight text-neutral-950 md:text-6xl">
|
||||
All the tools you need, in one workspace
|
||||
</h1>
|
||||
<p className="mt-4 leading-relaxed text-neutral-700">
|
||||
<p className="mt-5 max-w-2xl text-lg leading-8 text-[var(--color-brand-muted)]">
|
||||
Research keywords, track rankings, audit sites, and understand your AI
|
||||
visibility from one modern platform.
|
||||
</p>
|
||||
|
||||
<div className="mt-10 space-y-12">
|
||||
<div className="mt-12 space-y-12">
|
||||
<section>
|
||||
<div className="border-b border-neutral-200 pb-3">
|
||||
<h2 className="text-xl font-semibold text-neutral-900">
|
||||
<div className="border-b border-[var(--color-border-subtle)] pb-4">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
|
||||
AI agent workflows
|
||||
</h2>
|
||||
<p className="mt-1 text-sm leading-relaxed text-neutral-600">
|
||||
<p className="mt-2 max-w-2xl text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
Let supported MCP clients research keywords, SERPs, domains,
|
||||
backlinks, and first-party Search Console data through OpenSEO.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-5 grid gap-4 md:grid-cols-2">
|
||||
<a
|
||||
href="/features/mcp"
|
||||
className="block rounded-lg border border-neutral-200 bg-white p-5 transition-colors hover:border-neutral-900"
|
||||
>
|
||||
<p className="text-xs font-medium text-neutral-500">
|
||||
<FeatureCard href="/features/mcp">
|
||||
<p className="text-xs font-medium text-[var(--color-brand-accent)]">
|
||||
OpenSEO MCP
|
||||
</p>
|
||||
<h3 className="mt-2 text-lg font-semibold text-neutral-900">
|
||||
OpenSEO MCP for your AI agent
|
||||
<h3 className="mt-2 text-lg font-semibold text-neutral-950">
|
||||
OpenSEO MCP
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-neutral-600">
|
||||
Connect OpenSEO to Claude, Codex, and supported MCP clients so
|
||||
agents can call OpenSEO research tools with authorized project
|
||||
context.
|
||||
<p className="mt-2 text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
Connect Claude, Codex, and other agents to OpenSEO research
|
||||
tools.
|
||||
</p>
|
||||
<p className="mt-3 text-sm font-medium text-neutral-900">
|
||||
<p className="mt-4 text-sm font-medium text-neutral-950">
|
||||
Explore MCP <span aria-hidden="true">→</span>
|
||||
</p>
|
||||
</a>
|
||||
<a
|
||||
href="/google-search-console-mcp"
|
||||
className="block rounded-lg border border-neutral-200 bg-white p-5 transition-colors hover:border-neutral-900"
|
||||
>
|
||||
<p className="text-xs font-medium text-neutral-500">
|
||||
</FeatureCard>
|
||||
<FeatureCard href="/google-search-console-mcp">
|
||||
<p className="text-xs font-medium text-[var(--color-brand-accent)]">
|
||||
Search Console MCP
|
||||
</p>
|
||||
<h3 className="mt-2 text-lg font-semibold text-neutral-900">
|
||||
Google Search Console MCP — no Google Cloud setup
|
||||
<h3 className="mt-2 text-lg font-semibold text-neutral-950">
|
||||
Google Search Console MCP
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-neutral-600">
|
||||
Let your agent read clicks, impressions, CTR, position, and URL
|
||||
inspection data from your connected Search Console property.
|
||||
<p className="mt-2 text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
Give agents access to clicks, impressions, CTR, position, and
|
||||
URL inspection.
|
||||
</p>
|
||||
<p className="mt-3 text-sm font-medium text-neutral-900">
|
||||
<p className="mt-4 text-sm font-medium text-neutral-950">
|
||||
Explore GSC MCP <span aria-hidden="true">→</span>
|
||||
</p>
|
||||
</a>
|
||||
</FeatureCard>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{featureGroups.map((group) => (
|
||||
<section key={group.label}>
|
||||
<div className="border-b border-neutral-200 pb-3">
|
||||
<h2 className="text-xl font-semibold text-neutral-900">
|
||||
<div className="border-b border-[var(--color-border-subtle)] pb-4">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
|
||||
{group.label}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm leading-relaxed text-neutral-600">
|
||||
<p className="mt-2 max-w-2xl text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
{group.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-5 grid gap-4 md:grid-cols-2">
|
||||
<div className="mt-5 grid gap-4 md:grid-cols-3">
|
||||
{group.pages.map((page) => (
|
||||
<a
|
||||
key={page.slug}
|
||||
href={`/features/${page.slug}`}
|
||||
className="rounded-lg border border-neutral-200 bg-white p-5 transition-colors hover:border-neutral-900"
|
||||
>
|
||||
<p className="text-xs font-medium text-neutral-500">
|
||||
<FeatureCard key={page.slug} href={`/features/${page.slug}`}>
|
||||
<p className="text-xs font-medium text-[var(--color-brand-accent)]">
|
||||
{page.eyebrow}
|
||||
</p>
|
||||
<h3 className="mt-2 text-lg font-semibold text-neutral-900">
|
||||
<h3 className="mt-2 text-lg font-semibold text-neutral-950">
|
||||
{page.title}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-neutral-600">
|
||||
{page.description}
|
||||
<p className="mt-2 text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
{page.navDescription}
|
||||
</p>
|
||||
<p className="mt-3 text-sm font-medium text-neutral-900">
|
||||
<p className="mt-4 text-sm font-medium text-neutral-950">
|
||||
Explore feature <span aria-hidden="true">→</span>
|
||||
</p>
|
||||
</a>
|
||||
</FeatureCard>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function FeatureCard({
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
href: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className="block rounded-lg border border-[var(--color-border-subtle)] bg-white p-5 transition-colors hover:border-neutral-900"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@ -38,20 +38,23 @@ export const Route = createFileRoute("/_marketing/google-search-console-mcp")({
|
||||
|
||||
function GoogleSearchConsoleMcpPage() {
|
||||
return (
|
||||
<article className="text-neutral-900">
|
||||
<header className="mb-10 border-b border-neutral-200 pb-8">
|
||||
<h1 className="text-3xl font-bold leading-tight tracking-tight text-neutral-950 md:text-4xl">
|
||||
<article className="mx-auto max-w-4xl text-neutral-900">
|
||||
<header className="mb-10 border-b border-[var(--color-border-subtle)] pb-8">
|
||||
<p className="text-sm font-medium text-[var(--color-brand-accent)]">
|
||||
Search Console MCP
|
||||
</p>
|
||||
<h1 className="mt-3 text-4xl font-semibold leading-tight tracking-tight text-neutral-950 md:text-6xl">
|
||||
{frontmatter.title}
|
||||
</h1>
|
||||
{frontmatter.description ? (
|
||||
<p className="mt-4 text-lg leading-8 text-neutral-700">
|
||||
<p className="mt-5 max-w-2xl text-lg leading-8 text-[var(--color-brand-muted)]">
|
||||
{frontmatter.description}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
|
||||
<a
|
||||
href="https://app.openseo.so/sign-up"
|
||||
className="inline-flex h-10 items-center justify-center rounded-md bg-neutral-950 px-5 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
|
||||
className="inline-flex h-10 items-center justify-center rounded-lg bg-neutral-950 px-5 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
|
||||
>
|
||||
Get started
|
||||
<span className="ml-2" aria-hidden="true">
|
||||
@ -61,7 +64,7 @@ function GoogleSearchConsoleMcpPage() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<DocsBody className="min-w-0 text-neutral-800 [&_a]:!text-neutral-950 [&_h2]:!text-neutral-950 [&_h2_a]:!no-underline [&_h3]:!text-neutral-950 [&_h3_a]:!no-underline [&_h4]:!text-neutral-950 [&_h4_a]:!no-underline [&_h5_a]:!no-underline [&_h6_a]:!no-underline [&_li]:!text-neutral-700 [&_li_a]:font-medium [&_li_a]:underline [&_li_a]:decoration-neutral-300 [&_li_a]:underline-offset-4 [&_li_a:hover]:!text-neutral-700 [&_p]:!text-neutral-700 [&_p_a]:font-medium [&_p_a]:underline [&_p_a]:decoration-neutral-300 [&_p_a]:underline-offset-4 [&_p_a:hover]:!text-neutral-700 [&_strong]:!text-neutral-950">
|
||||
<DocsBody className="min-w-0 text-neutral-800 [&_a]:!text-neutral-950 [&_h2]:!text-neutral-950 [&_h2_a]:!no-underline [&_h3]:!text-neutral-950 [&_h3_a]:!no-underline [&_h4]:!text-neutral-950 [&_h4_a]:!no-underline [&_h5_a]:!no-underline [&_h6_a]:!no-underline [&_li]:!text-neutral-700 [&_li_a]:font-medium [&_li_a]:underline [&_li_a]:decoration-[var(--color-brand-accent)] [&_li_a]:underline-offset-4 [&_li_a:hover]:!text-neutral-700 [&_p]:!text-neutral-700 [&_p_a]:font-medium [&_p_a]:underline [&_p_a]:decoration-[var(--color-brand-accent)] [&_p_a]:underline-offset-4 [&_p_a:hover]:!text-neutral-700 [&_strong]:!text-neutral-950">
|
||||
<GoogleSearchConsoleMcpContent
|
||||
components={{ ...defaultMdxComponents, ComparisonTable }}
|
||||
/>
|
||||
@ -82,18 +85,18 @@ function GoogleSearchConsoleMcpPage() {
|
||||
|
||||
function GoogleSearchConsoleMcpCta() {
|
||||
return (
|
||||
<section className="mt-14 border-t border-neutral-200 pt-8">
|
||||
<p className="text-lg font-semibold text-neutral-950">
|
||||
<section className="mt-14 rounded-xl border border-[var(--color-border-subtle)] bg-white p-6">
|
||||
<p className="text-xl font-semibold tracking-tight text-neutral-950">
|
||||
Point your AI at your real search data
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-relaxed text-neutral-600">
|
||||
No Google Cloud project. Zero credits to read your own data. Works with
|
||||
Claude, Cursor, and Codex.
|
||||
<p className="mt-2 max-w-2xl text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
Free to connect. No Google Cloud project. Works with Claude, Codex,
|
||||
OpenClaw, OpenCode, and Gemini.
|
||||
</p>
|
||||
<div className="mt-5 flex flex-col gap-3 sm:flex-row">
|
||||
<a
|
||||
href="https://app.openseo.so/sign-up"
|
||||
className="inline-flex h-10 items-center justify-center rounded-md bg-neutral-950 px-4 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
|
||||
className="inline-flex h-10 items-center justify-center rounded-lg bg-neutral-950 px-4 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
|
||||
>
|
||||
Get started
|
||||
<span className="ml-2" aria-hidden="true">
|
||||
@ -104,7 +107,7 @@ function GoogleSearchConsoleMcpCta() {
|
||||
href="https://github.com/every-app/open-seo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex h-10 items-center justify-center gap-2 rounded-md border border-neutral-300 bg-white px-4 text-sm font-medium text-neutral-950 transition-colors hover:border-neutral-950"
|
||||
className="inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-[var(--color-border-subtle)] bg-white px-4 text-sm font-medium text-neutral-950 transition-colors hover:border-neutral-950"
|
||||
>
|
||||
<GitHubIcon />
|
||||
Star on GitHub
|
||||
|
||||
@ -1,144 +1,36 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { LandingPage } from "@/components/landing-page";
|
||||
import { buildPageSeo } from "@/lib/seo";
|
||||
|
||||
const homeTitle = "OpenSEO - Open Source SEO Platform";
|
||||
const homeDescription =
|
||||
"The open source SEO platform: keyword research, backlinks, rank tracking, and AI visibility. Self-host free (MIT) or use the managed app for $10/month.";
|
||||
"OpenSEO is the open source alternative to Ahrefs and Semrush. Keyword research, backlinks, rank tracking, and site audits, billed by usage instead of a $100-plus monthly subscription. Self-host it free, or connect it to your AI agents over MCP.";
|
||||
|
||||
export const Route = createFileRoute("/_marketing/")({
|
||||
head: () =>
|
||||
buildPageSeo({
|
||||
head: () => {
|
||||
const seo = buildPageSeo({
|
||||
title: homeTitle,
|
||||
description: homeDescription,
|
||||
path: "/",
|
||||
imageAlt: "OpenSEO keyword research dashboard preview",
|
||||
}),
|
||||
component: Home,
|
||||
});
|
||||
|
||||
return {
|
||||
...seo,
|
||||
links: [
|
||||
...(seo.links ?? []),
|
||||
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||
{
|
||||
rel: "preconnect",
|
||||
href: "https://fonts.gstatic.com",
|
||||
crossOrigin: "anonymous",
|
||||
},
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=JetBrains+Mono:wght@400;500&display=swap",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
component: LandingPage,
|
||||
});
|
||||
|
||||
// ─── Page ────────────────────────────────────────────────────────────
|
||||
|
||||
function Home() {
|
||||
return (
|
||||
<>
|
||||
{/* Headline */}
|
||||
<h1 className="text-3xl font-bold tracking-tight leading-tight">
|
||||
The Open Source SEO Platform
|
||||
</h1>
|
||||
|
||||
<p className="text-neutral-700 mt-4 leading-relaxed">
|
||||
Own your SEO — a self-hostable alternative to Semrush and Ahrefs.
|
||||
</p>
|
||||
|
||||
{/* Features */}
|
||||
<ul className="mt-5 space-y-3">
|
||||
{[
|
||||
"Keyword Research",
|
||||
"Backlink Analysis",
|
||||
"Competitor Insights",
|
||||
"Rank Tracking",
|
||||
"AI Visibility",
|
||||
].map((item) => (
|
||||
<li key={item} className="flex gap-2.5 text-sm text-neutral-800">
|
||||
<span className="text-neutral-500 mt-[2px]">—</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="mt-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<a
|
||||
href="https://app.openseo.so/sign-up"
|
||||
className="inline-flex items-center justify-center h-10 px-5 text-sm font-medium bg-neutral-900 text-white rounded-md hover:bg-neutral-800 transition-colors"
|
||||
>
|
||||
Try now
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/every-app/open-seo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center h-10 px-5 text-sm font-medium border border-neutral-300 text-neutral-900 rounded-md hover:border-neutral-900 transition-colors"
|
||||
>
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-500 mt-3">
|
||||
No credit card required.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex flex-col gap-8">
|
||||
{/* Self-host — after video on mobile, before on desktop */}
|
||||
<div className="order-2 md:order-1 rounded-lg border border-neutral-200 bg-neutral-50 px-5 py-5">
|
||||
<p className="text-sm font-semibold text-neutral-900">
|
||||
Self-host via Docker or Cloudflare
|
||||
</p>
|
||||
<p className="text-sm text-neutral-600 mt-2 leading-relaxed">
|
||||
100% open source (MIT). Bring your own DataForSEO API key. Fork and
|
||||
vibe code custom features for your workflow. See how OpenSEO
|
||||
compares to the other{" "}
|
||||
<a
|
||||
href="/blogs/best-open-source-seo-tools"
|
||||
className="font-medium text-neutral-900 hover:text-neutral-700 transition-colors underline decoration-neutral-300 underline-offset-4"
|
||||
>
|
||||
open source SEO tools
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/every-app/open-seo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 mt-3 text-sm font-medium text-neutral-900 hover:text-neutral-700 transition-colors"
|
||||
>
|
||||
View on GitHub
|
||||
<span aria-hidden="true">→</span>
|
||||
</a>
|
||||
<hr className="mt-4" />
|
||||
<p className="text-sm text-neutral-600 mt-2 leading-relaxed">
|
||||
Don't want to self-host?{" "}
|
||||
<a
|
||||
href="https://app.openseo.so/sign-up"
|
||||
className="font-medium text-neutral-900 hover:text-neutral-700 transition-colors"
|
||||
>
|
||||
Try the managed version.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Demo */}
|
||||
<div className="order-1 md:order-2">
|
||||
<video
|
||||
className="w-full rounded-md border border-neutral-200"
|
||||
width={1280}
|
||||
height={808}
|
||||
poster="/demo-poster.webp"
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
preload="none"
|
||||
aria-label="OpenSEO product demo"
|
||||
>
|
||||
<source src="/demo.webm" type="video/webm" />
|
||||
<source src="/demo.mp4" type="video/mp4" />
|
||||
<img
|
||||
src="/demo-poster.webp"
|
||||
alt="OpenSEO product demo"
|
||||
width={1280}
|
||||
height={808}
|
||||
className="w-full rounded-md border border-neutral-200"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</video>
|
||||
<p className="text-[11px] text-neutral-600 mt-2">
|
||||
Keyword research in OpenSEO
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -20,20 +20,22 @@ export const Route = createFileRoute("/_marketing/open-source-seo")({
|
||||
|
||||
function OpenSourceSeoPage() {
|
||||
return (
|
||||
<article className="text-neutral-900">
|
||||
<header className="mb-10 border-b border-neutral-200 pb-8">
|
||||
<p className="text-sm font-medium text-neutral-500">Open Source SEO</p>
|
||||
<h1 className="mt-3 text-3xl font-bold leading-tight tracking-tight text-neutral-950 md:text-4xl">
|
||||
<article className="mx-auto max-w-4xl text-neutral-900">
|
||||
<header className="mb-10 border-b border-[var(--color-border-subtle)] pb-8">
|
||||
<p className="text-sm font-medium text-[var(--color-brand-accent)]">
|
||||
Open Source SEO
|
||||
</p>
|
||||
<h1 className="mt-3 text-4xl font-semibold leading-tight tracking-tight text-neutral-950 md:text-6xl">
|
||||
{openSourceSeoFrontmatter.title}
|
||||
</h1>
|
||||
{openSourceSeoFrontmatter.description ? (
|
||||
<p className="mt-4 text-lg leading-8 text-neutral-700">
|
||||
<p className="mt-5 max-w-2xl text-lg leading-8 text-[var(--color-brand-muted)]">
|
||||
{openSourceSeoFrontmatter.description}
|
||||
</p>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<DocsBody className="min-w-0 text-neutral-800 [&_a]:!text-neutral-950 [&_h2]:!text-neutral-950 [&_h2_a]:!no-underline [&_h3]:!text-neutral-950 [&_h3_a]:!no-underline [&_h4]:!text-neutral-950 [&_h4_a]:!no-underline [&_h5_a]:!no-underline [&_h6_a]:!no-underline [&_li]:!text-neutral-700 [&_li_a]:font-medium [&_li_a]:underline [&_li_a]:decoration-neutral-300 [&_li_a]:underline-offset-4 [&_li_a:hover]:!text-neutral-700 [&_p]:!text-neutral-700 [&_p_a]:font-medium [&_p_a]:underline [&_p_a]:decoration-neutral-300 [&_p_a]:underline-offset-4 [&_p_a:hover]:!text-neutral-700 [&_strong]:!text-neutral-950">
|
||||
<DocsBody className="min-w-0 text-neutral-800 [&_a]:!text-neutral-950 [&_h2]:!text-neutral-950 [&_h2_a]:!no-underline [&_h3]:!text-neutral-950 [&_h3_a]:!no-underline [&_h4]:!text-neutral-950 [&_h4_a]:!no-underline [&_h5_a]:!no-underline [&_h6_a]:!no-underline [&_li]:!text-neutral-700 [&_li_a]:font-medium [&_li_a]:underline [&_li_a]:decoration-[var(--color-brand-accent)] [&_li_a]:underline-offset-4 [&_li_a:hover]:!text-neutral-700 [&_p]:!text-neutral-700 [&_p_a]:font-medium [&_p_a]:underline [&_p_a]:decoration-[var(--color-brand-accent)] [&_p_a]:underline-offset-4 [&_p_a:hover]:!text-neutral-700 [&_strong]:!text-neutral-950">
|
||||
<OpenSourceSeoContent components={defaultMdxComponents} />
|
||||
</DocsBody>
|
||||
|
||||
@ -44,18 +46,18 @@ function OpenSourceSeoPage() {
|
||||
|
||||
function OpenSourceSeoCta() {
|
||||
return (
|
||||
<section className="mt-14 border-t border-neutral-200 pt-8">
|
||||
<p className="text-lg font-semibold text-neutral-950">
|
||||
<section className="mt-14 rounded-xl border border-[var(--color-border-subtle)] bg-white p-6">
|
||||
<p className="text-xl font-semibold tracking-tight text-neutral-950">
|
||||
Try OpenSEO, or follow along on GitHub
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-relaxed text-neutral-600">
|
||||
<p className="mt-2 max-w-2xl text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
Try the hosted app if you want to get started right away. Or, check it
|
||||
out on GitHub. Make sure to give it a star!
|
||||
</p>
|
||||
<div className="mt-5 flex flex-col gap-3 sm:flex-row">
|
||||
<a
|
||||
href="https://app.openseo.so/sign-up"
|
||||
className="inline-flex h-10 items-center justify-center rounded-md bg-neutral-950 px-4 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
|
||||
className="inline-flex h-10 items-center justify-center rounded-lg bg-neutral-950 px-4 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
|
||||
>
|
||||
Try OpenSEO
|
||||
<span className="ml-2" aria-hidden="true">
|
||||
@ -66,7 +68,7 @@ function OpenSourceSeoCta() {
|
||||
href="https://github.com/every-app/open-seo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex h-10 items-center justify-center gap-2 rounded-md border border-neutral-300 bg-white px-4 text-sm font-medium text-neutral-950 transition-colors hover:border-neutral-950"
|
||||
className="inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-[var(--color-border-subtle)] bg-white px-4 text-sm font-medium text-neutral-950 transition-colors hover:border-neutral-950"
|
||||
>
|
||||
<GitHubIcon />
|
||||
Star on GitHub
|
||||
|
||||
@ -15,19 +15,31 @@ export const Route = createFileRoute("/_marketing/pricing")({
|
||||
|
||||
function Pricing() {
|
||||
return (
|
||||
<>
|
||||
{/* Headline */}
|
||||
<h1 className="text-3xl font-bold tracking-tight">Pricing</h1>
|
||||
<article className="mx-auto max-w-5xl">
|
||||
<p className="text-sm font-medium text-[var(--color-brand-accent)]">
|
||||
Pricing
|
||||
</p>
|
||||
<h1 className="mt-3 max-w-3xl text-4xl font-semibold leading-tight tracking-tight text-neutral-950 md:text-6xl">
|
||||
Start small, scale by usage
|
||||
</h1>
|
||||
<p className="mt-5 max-w-2xl text-lg leading-8 text-[var(--color-brand-muted)]">
|
||||
Use the managed app for $10/month, or self-host OpenSEO with your own
|
||||
API keys.
|
||||
</p>
|
||||
|
||||
{/* Managed */}
|
||||
<section className="mt-12">
|
||||
<h2 className="text-xl font-semibold">Managed</h2>
|
||||
<div className="mt-6 rounded-lg border border-neutral-200 overflow-hidden">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
|
||||
Managed
|
||||
</h2>
|
||||
<div className="mt-6 overflow-hidden rounded-xl border border-[var(--color-border-subtle)] bg-white">
|
||||
{/* Base Plan */}
|
||||
<div className="px-5 py-5">
|
||||
<div className="p-6">
|
||||
<div className="flex items-baseline justify-between gap-4">
|
||||
<p className="font-semibold">Base Plan</p>
|
||||
<p className="text-lg font-semibold tabular-nums">$10/month</p>
|
||||
<p className="font-semibold text-neutral-950">Base Plan</p>
|
||||
<p className="text-xl font-semibold tabular-nums text-neutral-950">
|
||||
$10/month
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mt-3 space-y-2">
|
||||
{[
|
||||
@ -39,8 +51,8 @@ function Pricing() {
|
||||
key={item}
|
||||
className="flex gap-2.5 text-sm text-neutral-700"
|
||||
>
|
||||
<span className="text-neutral-400 mt-[2px] shrink-0">
|
||||
—
|
||||
<span className="mt-[7px] h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--color-brand-accent)]">
|
||||
<span className="sr-only">Included:</span>
|
||||
</span>
|
||||
{item}
|
||||
</li>
|
||||
@ -49,14 +61,16 @@ function Pricing() {
|
||||
</div>
|
||||
|
||||
{/* Usage Credits — add-on, visually connected to Base Plan */}
|
||||
<div className="border-t border-dashed border-neutral-200 px-5 py-5">
|
||||
<div className="border-t border-dashed border-[var(--color-border-subtle)] bg-[#fbfaf8] p-6">
|
||||
<div className="flex items-baseline gap-3">
|
||||
<p className="text-sm font-semibold">Usage Credits</p>
|
||||
<span className="text-xs text-neutral-400 font-medium">
|
||||
<p className="text-sm font-semibold text-neutral-950">
|
||||
Usage Credits
|
||||
</p>
|
||||
<span className="text-xs font-medium text-[var(--color-brand-muted)]">
|
||||
add-on
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-neutral-600 leading-relaxed">
|
||||
<p className="mt-2 text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
Credits are consumed as you use the app. We use powerful providers
|
||||
for AI and SEO data. We bill based on usage of those APIs and
|
||||
charge a small premium.
|
||||
@ -70,8 +84,8 @@ function Pricing() {
|
||||
key={item}
|
||||
className="flex gap-2.5 text-sm text-neutral-700"
|
||||
>
|
||||
<span className="text-neutral-400 mt-[2px] shrink-0">
|
||||
—
|
||||
<span className="mt-[7px] h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--color-brand-accent)]">
|
||||
<span className="sr-only">Included:</span>
|
||||
</span>
|
||||
{item}
|
||||
</li>
|
||||
@ -81,7 +95,7 @@ function Pricing() {
|
||||
</div>
|
||||
<a
|
||||
href="https://app.openseo.so/sign-up"
|
||||
className="inline-flex items-center justify-center mt-4 rounded-lg bg-neutral-900 px-5 py-2.5 text-sm font-medium text-white hover:bg-neutral-800 transition-colors"
|
||||
className="mt-4 inline-flex items-center justify-center rounded-lg bg-neutral-950 px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
|
||||
>
|
||||
Get Started{" "}
|
||||
<span aria-hidden="true" className="ml-1.5">
|
||||
@ -91,11 +105,13 @@ function Pricing() {
|
||||
</section>
|
||||
|
||||
{/* Self-hosted */}
|
||||
<section className="mt-12">
|
||||
<section className="mt-12 rounded-xl border border-[var(--color-border-subtle)] bg-white p-6">
|
||||
<div className="flex items-baseline justify-between gap-4">
|
||||
<h2 className="text-xl font-semibold">Self-hosted</h2>
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
|
||||
Self-hosted
|
||||
</h2>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-neutral-600 leading-relaxed">
|
||||
<p className="mt-3 max-w-2xl text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
Deploy OpenSEO yourself via Docker or Cloudflare Workers. Bring your
|
||||
own API keys and pay DataForSEO and other API providers directly.
|
||||
</p>
|
||||
@ -103,7 +119,7 @@ function Pricing() {
|
||||
href="https://github.com/every-app/open-seo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 mt-3 text-sm font-medium text-neutral-900 hover:text-neutral-600 transition-colors"
|
||||
className="mt-4 inline-flex items-center gap-1.5 text-sm font-medium text-neutral-950 transition-colors hover:text-[var(--color-brand-accent)]"
|
||||
>
|
||||
View on GitHub
|
||||
<span aria-hidden="true">→</span>
|
||||
@ -112,54 +128,60 @@ function Pricing() {
|
||||
|
||||
{/* FAQ */}
|
||||
<section className="mt-12">
|
||||
<h2 className="text-xl font-semibold">FAQ</h2>
|
||||
<dl className="mt-4 space-y-6">
|
||||
<div>
|
||||
<dt className="text-sm font-medium">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
|
||||
FAQ
|
||||
</h2>
|
||||
<dl className="mt-5 divide-y divide-[var(--color-border-subtle)] rounded-xl border border-[var(--color-border-subtle)] bg-white">
|
||||
<div className="p-5">
|
||||
<dt className="text-sm font-medium text-neutral-950">
|
||||
What if I use all my included credits?
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-neutral-600 leading-relaxed">
|
||||
<dd className="mt-1.5 text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
You'll never have unexpected costs or bills. If you use all your
|
||||
credits, you'll see errors when you try to do tasks. You can
|
||||
purchase more top up credits at any time.
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium">What features use credits?</dt>
|
||||
<dd className="mt-1 text-sm text-neutral-600 leading-relaxed">
|
||||
<div className="p-5">
|
||||
<dt className="text-sm font-medium text-neutral-950">
|
||||
What features use credits?
|
||||
</dt>
|
||||
<dd className="mt-1.5 text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
Credits are consumed by features that query DataForSEO's API —
|
||||
backlinks, keyword volume, competitor data, and site audits. Your
|
||||
projects, settings, and any data already fetched don't cost
|
||||
credits.
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium">
|
||||
<div className="p-5">
|
||||
<dt className="text-sm font-medium text-neutral-950">
|
||||
Do unused credits roll over?
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-neutral-600 leading-relaxed">
|
||||
<dd className="mt-1.5 text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
Top-up credits roll over indefinitely. The Usage Credits included
|
||||
with your Base Plan reset each billing cycle.
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium">Can I cancel anytime?</dt>
|
||||
<dd className="mt-1 text-sm text-neutral-600 leading-relaxed">
|
||||
<div className="p-5">
|
||||
<dt className="text-sm font-medium text-neutral-950">
|
||||
Can I cancel anytime?
|
||||
</dt>
|
||||
<dd className="mt-1.5 text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
Yes. Cancel from your billing portal at any time. Your access
|
||||
continues through the end of the current billing period.{" "}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium">
|
||||
<div className="p-5">
|
||||
<dt className="text-sm font-medium text-neutral-950">
|
||||
Do I need a subscription or just usage credits?
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm text-neutral-600 leading-relaxed">
|
||||
<dd className="mt-1.5 text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
While top-up Usage Credits roll over and don't expire, you need an
|
||||
active subscription in order to use OpenSEO.
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
</>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { HomeLayout } from "fumadocs-ui/layouts/home";
|
||||
import { createClientLoader } from "fumadocs-mdx/runtime/vite";
|
||||
import { DocsBody } from "fumadocs-ui/page";
|
||||
import defaultMdxComponents from "fumadocs-ui/mdx";
|
||||
import { baseOptions } from "@/lib/layout.shared";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { BlogLayout } from "@/components/blog-layout";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import { Suspense } from "react";
|
||||
import { getBlogPost } from "@/lib/content.functions";
|
||||
import { blog } from "../../../source.generated";
|
||||
@ -37,10 +38,13 @@ const clientMdxLoader = createClientLoader(blog, {
|
||||
id: "blog",
|
||||
component({ default: MDX }) {
|
||||
return (
|
||||
<DocsBody className="text-fd-foreground [&_h2]:text-fd-foreground [&_h3]:text-fd-foreground [&_li]:text-fd-foreground/90 [&_p]:text-fd-foreground/90 [&_strong]:text-fd-foreground">
|
||||
<DocsBody className="text-neutral-800 [&_a]:!text-neutral-950 [&_a]:underline [&_a]:decoration-[var(--color-brand-accent)] [&_a]:underline-offset-4 [&_h2]:text-neutral-950 [&_h2_a]:!no-underline [&_h3]:text-neutral-950 [&_h3_a]:!no-underline [&_li]:text-neutral-700 [&_p]:text-neutral-700 [&_strong]:text-neutral-950">
|
||||
<MDX
|
||||
components={{
|
||||
...defaultMdxComponents,
|
||||
table: BlogTable,
|
||||
th: BlogTableHeader,
|
||||
td: BlogTableCell,
|
||||
}}
|
||||
/>
|
||||
</DocsBody>
|
||||
@ -48,6 +52,35 @@ const clientMdxLoader = createClientLoader(blog, {
|
||||
},
|
||||
});
|
||||
|
||||
function BlogTable(props: ComponentPropsWithoutRef<"table">) {
|
||||
return (
|
||||
<div className="not-prose my-8 w-full max-w-full overflow-x-auto rounded-xl border border-[var(--color-border-subtle)] bg-white">
|
||||
<table
|
||||
{...props}
|
||||
className="w-full min-w-[720px] border-collapse text-left text-sm"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BlogTableHeader(props: ComponentPropsWithoutRef<"th">) {
|
||||
return (
|
||||
<th
|
||||
{...props}
|
||||
className="border-b border-r border-neutral-200 bg-neutral-950 px-4 py-3 text-left text-sm font-semibold text-white last:border-r-0"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BlogTableCell(props: ComponentPropsWithoutRef<"td">) {
|
||||
return (
|
||||
<td
|
||||
{...props}
|
||||
className="border-b border-r border-neutral-200 px-4 py-3 align-top text-sm leading-6 text-neutral-700 last:border-r-0 [&_a]:font-medium [&_a]:!text-neutral-950"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BlogPost() {
|
||||
const data = Route.useLoaderData() as {
|
||||
path: string;
|
||||
@ -57,14 +90,18 @@ function BlogPost() {
|
||||
const Content = clientMdxLoader.getComponent(data.path);
|
||||
|
||||
return (
|
||||
<HomeLayout {...baseOptions()}>
|
||||
<article className="max-w-3xl mx-auto px-6 py-12 md:py-24 text-fd-foreground">
|
||||
<BlogLayout>
|
||||
<article className="mx-auto max-w-3xl px-6 py-12 text-neutral-950 md:py-24">
|
||||
<BlogHeader title={data.title} description={data.description} />
|
||||
<Suspense>
|
||||
<Content />
|
||||
</Suspense>
|
||||
|
||||
<div className="mt-16 border-t border-[var(--color-border-subtle)] pt-8">
|
||||
<SiteFooter className="text-xs text-neutral-600 [&_a]:transition-colors [&_a]:hover:text-neutral-900" />
|
||||
</div>
|
||||
</article>
|
||||
</HomeLayout>
|
||||
</BlogLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -76,21 +113,21 @@ function BlogHeader({
|
||||
description?: string;
|
||||
}) {
|
||||
return (
|
||||
<header className="mb-8">
|
||||
<header className="mb-10 border-b border-[var(--color-border-subtle)] pb-8">
|
||||
<div className="mb-4">
|
||||
<Link
|
||||
to="/blogs"
|
||||
className="inline-flex items-center gap-2 text-sm font-medium text-fd-muted-foreground transition-colors hover:text-fd-primary"
|
||||
className="inline-flex items-center gap-2 text-sm font-medium text-[var(--color-brand-muted)] transition-colors hover:text-neutral-950"
|
||||
>
|
||||
<span aria-hidden="true">←</span>
|
||||
<span>Back to Blog</span>
|
||||
</Link>
|
||||
</div>
|
||||
<h1 className="mb-4 text-4xl font-bold text-fd-foreground md:text-5xl">
|
||||
<h1 className="mb-5 text-4xl font-semibold leading-tight tracking-tight text-neutral-950 md:text-6xl">
|
||||
{title}
|
||||
</h1>
|
||||
{description && (
|
||||
<p className="max-w-2xl text-lg leading-8 text-fd-muted-foreground md:text-xl">
|
||||
<p className="max-w-2xl text-lg leading-8 text-[var(--color-brand-muted)]">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { HomeLayout } from "fumadocs-ui/layouts/home";
|
||||
import { baseOptions } from "@/lib/layout.shared";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { BlogLayout } from "@/components/blog-layout";
|
||||
import { getBlogPosts } from "@/lib/content.functions";
|
||||
import { buildPageSeo } from "@/lib/seo";
|
||||
|
||||
const blogIndexDescription = "Founder-focused SEO articles from OpenSEO.";
|
||||
const blogIndexDescription = "SEO articles and guides from OpenSEO.";
|
||||
|
||||
export const Route = createFileRoute("/blogs/")({
|
||||
head: () =>
|
||||
@ -21,37 +21,49 @@ function BlogIndex() {
|
||||
const posts = Route.useLoaderData();
|
||||
|
||||
return (
|
||||
<HomeLayout {...baseOptions()}>
|
||||
<div className="max-w-3xl mx-auto px-6 py-12 md:py-24">
|
||||
<h1 className="text-4xl font-bold mb-8">Blog</h1>
|
||||
<BlogLayout>
|
||||
<div className="mx-auto max-w-5xl px-6 py-12 md:py-24">
|
||||
<p className="text-sm font-medium text-[var(--color-brand-accent)]">
|
||||
Resources
|
||||
</p>
|
||||
<h1 className="mt-3 text-4xl font-semibold tracking-tight text-neutral-950 md:text-6xl">
|
||||
Blog
|
||||
</h1>
|
||||
|
||||
{posts.length === 0 ? (
|
||||
<p className="text-fd-muted-foreground">
|
||||
<p className="mt-8 text-[var(--color-brand-muted)]">
|
||||
No posts yet. Check back soon.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
<div className="mt-10 grid gap-4 md:grid-cols-2">
|
||||
{posts.map((post) => (
|
||||
<article key={post.url} className="border-b pb-8 last:border-b-0">
|
||||
<article key={post.url}>
|
||||
<Link
|
||||
to="/blogs/$"
|
||||
params={{ _splat: post.slugs.join("/") }}
|
||||
className="group"
|
||||
className="group block h-full rounded-lg border border-[var(--color-border-subtle)] bg-white p-6 transition-colors hover:border-neutral-900"
|
||||
>
|
||||
<h2 className="text-2xl font-semibold group-hover:text-fd-primary transition-colors mb-2">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950 transition-colors group-hover:text-[var(--color-brand-accent)]">
|
||||
{post.title}
|
||||
</h2>
|
||||
{post.description && (
|
||||
<p className="text-fd-muted-foreground">
|
||||
<p className="mt-3 text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||
{post.description}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-5 text-sm font-medium text-neutral-950">
|
||||
Read post <span aria-hidden="true">→</span>
|
||||
</p>
|
||||
</Link>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-16 border-t border-[var(--color-border-subtle)] pt-8">
|
||||
<SiteFooter className="text-xs text-neutral-600 [&_a]:transition-colors [&_a]:hover:text-neutral-900" />
|
||||
</div>
|
||||
</HomeLayout>
|
||||
</div>
|
||||
</BlogLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,15 +3,20 @@
|
||||
@import "fumadocs-ui/css/preset.css";
|
||||
|
||||
@theme {
|
||||
--color-brand: #1a1a1a;
|
||||
--color-brand-muted: #6b7280;
|
||||
--color-brand-accent: #2563eb;
|
||||
--color-surface: #fafafa;
|
||||
--color-brand: #111111;
|
||||
--color-brand-muted: #66625e;
|
||||
--color-brand-accent: #ff5600;
|
||||
--color-surface: #f5f1ec;
|
||||
--color-surface-raised: #ffffff;
|
||||
--color-border-subtle: #e5e7eb;
|
||||
--color-border-subtle: #d8d1c8;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background: var(--color-fd-background);
|
||||
color: var(--color-fd-foreground);
|
||||
font-family:
|
||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", sans-serif;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||