diff --git a/web/content/blogs/seo-for-startups.md b/web/content/blogs/seo-for-startups.md index 231fc2a..4b01f98 100644 --- a/web/content/blogs/seo-for-startups.md +++ b/web/content/blogs/seo-for-startups.md @@ -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) -![SEO for Startups infographic](https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/cb1e47dc-8963-4338-e8d9-1ea60c0cd100/public) +![SEO for Startups infographic](/blog/seo-for-startups/infographic.png) ## 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). -![OpenSEO domain overview showing top pages, keywords, and estimated traffic](https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/85c0c3bf-fe49-4e18-998b-5a5017304100/public) +![OpenSEO domain overview showing top pages, keywords, and estimated traffic](/blog/seo-for-startups/domain-overview.png) ### 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. -![OpenSEO keyword research view showing related or suggested keywords for a seed term](https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/f8739457-4cc0-4f99-8c7a-7c74b35f0d00/public) +![OpenSEO keyword research view showing related or suggested keywords for a seed term](/blog/seo-for-startups/keyword-research.png) ### 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. -![OpenSEO backlink analysis showing referring domains or top backlinks for a competitor](https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/d9a2a5ea-6aa4-469e-10b5-e413febe4200/public) +![OpenSEO backlink analysis showing referring domains or top backlinks for a competitor](/blog/seo-for-startups/backlink-analysis.png) ## How can you improve your site's rankings? diff --git a/web/public/avatars/fed-avatar.jpg b/web/public/avatars/fed-avatar.jpg new file mode 100644 index 0000000..e32b429 Binary files /dev/null and b/web/public/avatars/fed-avatar.jpg differ diff --git a/web/public/avatars/samik-avatar.jpg b/web/public/avatars/samik-avatar.jpg new file mode 100644 index 0000000..2709c97 Binary files /dev/null and b/web/public/avatars/samik-avatar.jpg differ diff --git a/web/public/avatars/tom-avatar.jpeg b/web/public/avatars/tom-avatar.jpeg new file mode 100644 index 0000000..4e6c18f Binary files /dev/null and b/web/public/avatars/tom-avatar.jpeg differ diff --git a/web/public/blog/seo-for-startups/backlink-analysis.png b/web/public/blog/seo-for-startups/backlink-analysis.png new file mode 100644 index 0000000..c0c903b Binary files /dev/null and b/web/public/blog/seo-for-startups/backlink-analysis.png differ diff --git a/web/public/blog/seo-for-startups/domain-overview.png b/web/public/blog/seo-for-startups/domain-overview.png new file mode 100644 index 0000000..b7ab0e1 Binary files /dev/null and b/web/public/blog/seo-for-startups/domain-overview.png differ diff --git a/web/public/blog/seo-for-startups/infographic.png b/web/public/blog/seo-for-startups/infographic.png new file mode 100644 index 0000000..bbac27c Binary files /dev/null and b/web/public/blog/seo-for-startups/infographic.png differ diff --git a/web/public/blog/seo-for-startups/keyword-research.png b/web/public/blog/seo-for-startups/keyword-research.png new file mode 100644 index 0000000..3056e3a Binary files /dev/null and b/web/public/blog/seo-for-startups/keyword-research.png differ diff --git a/web/public/demo-poster.webp b/web/public/demo-poster.webp index 9505c09..5c96234 100644 Binary files a/web/public/demo-poster.webp and b/web/public/demo-poster.webp differ diff --git a/web/public/demo.gif b/web/public/demo.gif deleted file mode 100644 index 47c0eeb..0000000 Binary files a/web/public/demo.gif and /dev/null differ diff --git a/web/public/demo.mp4 b/web/public/demo.mp4 index fb3ee5c..d142436 100644 Binary files a/web/public/demo.mp4 and b/web/public/demo.mp4 differ diff --git a/web/public/demo.webm b/web/public/demo.webm deleted file mode 100644 index e234581..0000000 Binary files a/web/public/demo.webm and /dev/null differ diff --git a/web/src/components/blog-layout.tsx b/web/src/components/blog-layout.tsx new file mode 100644 index 0000000..b56ae63 --- /dev/null +++ b/web/src/components/blog-layout.tsx @@ -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 ( +
+
+
+ + OpenSEO + + + +
+
+ + {children} +
+ ); +} diff --git a/web/src/components/comparison-table.tsx b/web/src/components/comparison-table.tsx index 3c85ded..ec929f1 100644 --- a/web/src/components/comparison-table.tsx +++ b/web/src/components/comparison-table.tsx @@ -77,7 +77,7 @@ const ROWS: { label: string; cells: Cell[] }[] = [ export function ComparisonTable() { return (
-
+
@@ -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() { @@ -111,8 +111,10 @@ export function ComparisonTable() { return (
{row.label} @@ -141,14 +143,16 @@ function CellContent({ cell, highlight }: { cell: Cell; highlight?: boolean }) {
{cell.code ? ( - + {cell.text} ) : ( diff --git a/web/src/components/feature-page.tsx b/web/src/components/feature-page.tsx index a991b41..a86f59a 100644 --- a/web/src/components/feature-page.tsx +++ b/web/src/components/feature-page.tsx @@ -6,21 +6,26 @@ type FeaturePageProps = { export function FeaturePageTemplate({ page }: FeaturePageProps) { return ( -
-
-

{page.eyebrow}

-

+
+
+

+ {page.eyebrow} +

+

{page.title}

-

+

{page.description}

@@ -28,87 +33,52 @@ export function FeaturePageTemplate({ page }: FeaturePageProps) {
-

What you can do

-
    +

    + What you can do +

    +
      {page.workflows.map((workflow, index) => (
    1. - + {String(index + 1).padStart(2, "0")} -
      -

      - {workflow.title} -

      -

      - {workflow.description} -

      -
      +

      + {workflow.title} +

      +

      + {workflow.description} +

    2. ))}
-
-

Data you can act on

-
- {page.metrics.map((metric, index) => ( -
-
{metric.label}
-
- {metric.value} -
-
- ))} -
-
+ {page.showMetrics ? : null} + +
+ + +
-

Use cases

-
    - {page.useCases.map((item) => ( -
  • - - {item} -
  • - ))} -
-
- -
-

Why OpenSEO

-
    - {page.differentiators.map((item) => ( -
  • - - {item} -
  • - ))} -
-
- -
-

Related features

+

+ Related features +

{page.related.map((item) => ( {item.label} - @@ -117,14 +87,16 @@ export function FeaturePageTemplate({ page }: FeaturePageProps) {
-

FAQ

-
+

+ FAQ +

+
{page.faqs.map((faq) => ( -
+

{faq.question}

-

+

{faq.answer}

@@ -132,17 +104,22 @@ export function FeaturePageTemplate({ page }: FeaturePageProps) {
-
-

Try OpenSEO

-

+

+

+ Try OpenSEO +

+

The open source alternative to bloated, expensive, legacy SEO tools.

@@ -152,7 +129,7 @@ export function FeaturePageTemplate({ page }: FeaturePageProps) { function FeatureImage({ page }: FeaturePageProps) { return ( -
+
{page.imageAlt} -
+
{page.eyebrow} in OpenSEO.
); } + +function MetricsSection({ page }: FeaturePageProps) { + return ( +
+

+ Data you can act on +

+
+ {page.metrics.map((metric, 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(" ")} + > +
+ {metric.label} +
+
+ {metric.value} +
+
+ ))} +
+
+ ); +} + +function ListSection({ title, items }: { title: string; items: string[] }) { + return ( +
+

+ {title} +

+
    + {items.map((item) => ( +
  • +
  • + ))} +
+
+ ); +} diff --git a/web/src/components/landing-page.css b/web/src/components/landing-page.css new file mode 100644 index 0000000..f243f42 --- /dev/null +++ b/web/src/components/landing-page.css @@ -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
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; + } +} diff --git a/web/src/components/landing-page.tsx b/web/src/components/landing-page.tsx new file mode 100644 index 0000000..9474636 --- /dev/null +++ b/web/src/components/landing-page.tsx @@ -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 ( + + + + + ); +} + +function IconGithub({ size = 16, className }: IconProps) { + return ( + + ); +} + +function IconX({ size = 14, className }: IconProps) { + return ( + + ); +} + +function IconLinkedIn({ size = 14, className }: IconProps) { + return ( + + ); +} + +function IconLink({ size = 14, className }: IconProps) { + return ( + + + + + ); +} + +function IconDiscord({ size = 18, className }: IconProps) { + return ( + + ); +} + +// ─── Shared bits ───────────────────────────────────────────────────── + +function Container({ + children, + className = "", +}: { + children: ReactNode; + className?: string; +}) { + return
{children}
; +} + +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 ( + + {children} + + + ); +} + +// ─── Hero ──────────────────────────────────────────────────────────── + +function Hero() { + return ( +
+ +

+ Say goodbye to{" "} + bloated,{" "} +
+ overpriced SEO tools. +

+

+ Speed up strategy decisions with quality SEO data for every workflow, + all in one place.{" "} + + Don't put SEO on autopilot + + ,{" "} + + work with your AI agent to actually win. + +

+
+ +
+
+
+ ); +} + +// ─── Testimonial (true-black inverse strip) ────────────────────────── + +function Testimonial() { + return ( +
+ +
+

+ Trusted by customers all over the world +

+
+ {TESTIMONIALS.map((t) => ( +
+ +
{t.quote}
+
+ + + {t.name} + + {t.network === "x" ? ( + + ) : t.network === "linkedin" ? ( + + ) : ( + + )} + {t.handle} + + +
+
+ ))} +
+
+
+
+ ); +} + +// ─── 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 ( + + ); +} + +function ProductSection() { + return ( +
+ +
+

See OpenSEO in action

+

+ Keyword research, competitor analysis, backlinks, rank tracking, + technical audits, and AI-search visibility, all on real DataForSEO + data and connected to each other. +

+
+ +
+
+ +
+
+ +
+ {FEATURE_CARDS.map(({ page, blurb }) => ( + +

+ {page.eyebrow} +

+

+ {blurb} +

+
+ ))} +
+ + +
+
+ ); +} + +// ─── MCP: the page's one Fin Orange moment ─────────────────────────── + +type McpClient = { + name: string; + Icon: (props: SVGProps) => 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 ( +
+ +
+
+

+ Model Context Protocol +

+

Get superpowers with the MCP

+

+ 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. +

+
+ {MCP_CLIENTS.map(({ name, Icon }) => ( + + + ))} +
+ +
+ +
+
+
+
+              
+                 find and cluster keywords
+                for{" "}
+                openseo.so
+                {"\n\n"}
+                
+                  ⏺ openseo.keyword_research(seed: "open source seo")
+                
+                {"\n"}
+                {"  "}keyword{"                      "}volume{"     "}kd{"\n"}
+                {"  "}open source seo{"              "}
+                1,300
+                {"      "}
+                12
+                {"\n"}
+                {"  "}open source seo tools{"        "}
+                720
+                {"        "}
+                9
+                {"\n"}
+                {"  "}self-hosted seo platform{"     "}
+                210
+                {"        "}
+                4
+                {"\n\n"}
+                
+                
+                  {" "}
+                  Saved 3 keywords to your workspace.
+                
+                {"\n"}
+                
+                 View data in app: 
+                app.openseo.so/keywords
+              
+            
+
+
+
+
+ ); +} + +function ClaudeIcon(props: SVGProps) { + return ( + + + + ); +} + +function CodexIcon(props: SVGProps) { + return ( + + + + ); +} + +function OpenClawIcon(props: SVGProps) { + return ( + + + + + + + + + + + + + + + + + + ); +} + +function OpenCodeIcon(props: SVGProps) { + return ( + + + + + + ); +} + +function GeminiIcon(props: SVGProps) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +// ─── Open source ───────────────────────────────────────────────────── + +function OpenSourceSection() { + return ( +
+ +
+

100% open source

+

+ 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. +

+
+ + +
+
+ ); +} + +// ─── Footer ────────────────────────────────────────────────────────── + +function Footer() { + return ( +
+ +
+
+

+ Stay in the loop +

+

+ Product updates, new features, and the occasional + behind-the-scenes. +

+
+
+ +
+
+ +
+ +
+ +

+ © 2026 Every App, Inc. +

+
+
+ ); +} + +// ─── Page ──────────────────────────────────────────────────────────── + +export function LandingPage() { + return ( +
+ + + + + +
+ ); +} diff --git a/web/src/components/legal-page.tsx b/web/src/components/legal-page.tsx index 252407b..f695cc4 100644 --- a/web/src/components/legal-page.tsx +++ b/web/src/components/legal-page.tsx @@ -13,17 +13,23 @@ type LegalPageProps = { export function LegalPage({ title, description, children }: LegalPageProps) { return ( -
-
-

{title}

+
+
+

+ {title} +

{description ? ( -

{description}

+

+ {description} +

) : null}
- {children} + + {children} + -
+
diff --git a/web/src/components/newsletter-signup.tsx b/web/src/components/newsletter-signup.tsx new file mode 100644 index 0000000..3a7f436 --- /dev/null +++ b/web/src/components/newsletter-signup.tsx @@ -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

You're on the list.

; + } + + return ( +
+
+ + 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" + /> + +
+ {status === "error" && ( +

{errorMessage}

+ )} +
+ ); +} diff --git a/web/src/lib/feature-pages.ts b/web/src/lib/feature-pages.ts index 4106cd5..cecba57 100644 --- a/web/src/lib/feature-pages.ts +++ b/web/src/lib/feature-pages.ts @@ -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.", diff --git a/web/src/lib/layout.shared.tsx b/web/src/lib/layout.shared.tsx index aaa62a9..b7e46ac 100644 --- a/web/src/lib/layout.shared.tsx +++ b/web/src/lib/layout.shared.tsx @@ -15,7 +15,7 @@ export function baseOptions(): BaseLayoutProps { items: [ { text: "Blog", - description: "Founder-focused SEO articles.", + description: "SEO articles and guides.", url: "/blogs", }, { diff --git a/web/src/routes/_marketing.tsx b/web/src/routes/_marketing.tsx index c80f2e9..2648697 100644 --- a/web/src/routes/_marketing.tsx +++ b/web/src/routes/_marketing.tsx @@ -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 ( -
-
+
+
-
); } @@ -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 ( -
+
-
-
+
+
{resources.map((resource) => ( {resource.label} @@ -258,7 +282,7 @@ function ResourcesDropdown() { function FeatureDropdown() { return ( -
+
-
-
+
+
{featureGroups.map((group) => (
@@ -284,7 +308,7 @@ function FeatureDropdown() { {page.eyebrow} @@ -304,7 +328,7 @@ function FeatureDropdown() {
OpenSEO MCP @@ -315,18 +339,18 @@ function FeatureDropdown() { Search Console MCP - Free first-party GSC data for agents. + Search Console data for agents. View all features @@ -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 */} -
+

Stay in the loop

@@ -389,42 +375,13 @@ function MarketingFooter() { Product updates, new features, and the occasional behind-the-scenes.

- {nl.status === "success" ? ( -

You're on the list.

- ) : ( -
-
- 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"} - /> - -
- {nl.status === "error" && ( -

{nl.errorMessage}

- )} -
- )} +
{/* Footer */}
- +
); diff --git a/web/src/routes/_marketing/features/index.tsx b/web/src/routes/_marketing/features/index.tsx index 2362ec4..108df1b 100644 --- a/web/src/routes/_marketing/features/index.tsx +++ b/web/src/routes/_marketing/features/index.tsx @@ -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 ( - <> -

+

+

Open-source SEO tools

-

- A delightful suite of SEO tools +

+ All the tools you need, in one workspace

-

+

Research keywords, track rankings, audit sites, and understand your AI visibility from one modern platform.

-
+
-
-

+
+

AI agent workflows

-

+

Let supported MCP clients research keywords, SERPs, domains, backlinks, and first-party Search Console data through OpenSEO.

{featureGroups.map((group) => (
-
-

+
+

{group.label}

-

+

{group.description}

-

))}
- +
+ ); +} + +function FeatureCard({ + href, + children, +}: { + href: string; + children: ReactNode; +}) { + return ( + + {children} + ); } diff --git a/web/src/routes/_marketing/google-search-console-mcp.tsx b/web/src/routes/_marketing/google-search-console-mcp.tsx index ebef3af..dfc62df 100644 --- a/web/src/routes/_marketing/google-search-console-mcp.tsx +++ b/web/src/routes/_marketing/google-search-console-mcp.tsx @@ -38,20 +38,23 @@ export const Route = createFileRoute("/_marketing/google-search-console-mcp")({ function GoogleSearchConsoleMcpPage() { return ( -
-
-

+
+
+

+ Search Console MCP +

+

{frontmatter.title}

{frontmatter.description ? ( -

+

{frontmatter.description}

) : null}
- + @@ -82,18 +85,18 @@ function GoogleSearchConsoleMcpPage() { function GoogleSearchConsoleMcpCta() { return ( -
-

+

+

Point your AI at your real search data

-

- No Google Cloud project. Zero credits to read your own data. Works with - Claude, Cursor, and Codex. +

+ Free to connect. No Google Cloud project. Works with Claude, Codex, + OpenClaw, OpenCode, and Gemini.

Get started
- -

- No credit card required. -

-
- -
- {/* Self-host — after video on mobile, before on desktop */} -
-

- Self-host via Docker or Cloudflare -

-

- 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{" "} - - open source SEO tools - - . -

- - View on GitHub - - -
-

- Don't want to self-host?{" "} - - Try the managed version. - -

-
- - {/* Demo */} -
- -

- Keyword research in OpenSEO -

-
-
- - ); -} diff --git a/web/src/routes/_marketing/open-source-seo.tsx b/web/src/routes/_marketing/open-source-seo.tsx index 32b5221..2876805 100644 --- a/web/src/routes/_marketing/open-source-seo.tsx +++ b/web/src/routes/_marketing/open-source-seo.tsx @@ -20,20 +20,22 @@ export const Route = createFileRoute("/_marketing/open-source-seo")({ function OpenSourceSeoPage() { return ( -
-
-

Open Source SEO

-

+
+
+

+ Open Source SEO +

+

{openSourceSeoFrontmatter.title}

{openSourceSeoFrontmatter.description ? ( -

+

{openSourceSeoFrontmatter.description}

) : null}
- + @@ -44,18 +46,18 @@ function OpenSourceSeoPage() { function OpenSourceSeoCta() { return ( -
-

+

+

Try OpenSEO, or follow along on GitHub

-

+

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!

Try OpenSEO
+

+ Pricing +

+

+ Start small, scale by usage +

+

+ Use the managed app for $10/month, or self-host OpenSEO with your own + API keys. +

{/* Managed */}
-

Managed

-
{/* Self-hosted */} -
+
-

Self-hosted

+

+ Self-hosted +

-

+

Deploy OpenSEO yourself via Docker or Cloudflare Workers. Bring your own API keys and pay DataForSEO and other API providers directly.

@@ -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 @@ -112,54 +128,60 @@ function Pricing() { {/* FAQ */}
-

FAQ

-
-
-
+

+ FAQ +

+
+
+
What if I use all my included credits?
-
+
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.
-
-
What features use credits?
-
+
+
+ What features use credits? +
+
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.
-
-
+
+
Do unused credits roll over?
-
+
Top-up credits roll over indefinitely. The Usage Credits included with your Base Plan reset each billing cycle.
-
-
Can I cancel anytime?
-
+
+
+ Can I cancel anytime? +
+
Yes. Cancel from your billing portal at any time. Your access continues through the end of the current billing period.{" "}
-
-
+
+
Do I need a subscription or just usage credits?
-
+
While top-up Usage Credits roll over and don't expire, you need an active subscription in order to use OpenSEO.
- +
); } diff --git a/web/src/routes/blogs/$.tsx b/web/src/routes/blogs/$.tsx index 552cbc8..ebc9bdf 100644 --- a/web/src/routes/blogs/$.tsx +++ b/web/src/routes/blogs/$.tsx @@ -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 ( - + @@ -48,6 +52,35 @@ const clientMdxLoader = createClientLoader(blog, { }, }); +function BlogTable(props: ComponentPropsWithoutRef<"table">) { + return ( +
+ + + ); +} + +function BlogTableHeader(props: ComponentPropsWithoutRef<"th">) { + return ( +
+ ); +} + +function BlogTableCell(props: ComponentPropsWithoutRef<"td">) { + return ( + + ); +} + function BlogPost() { const data = Route.useLoaderData() as { path: string; @@ -57,14 +90,18 @@ function BlogPost() { const Content = clientMdxLoader.getComponent(data.path); return ( - -
+ +
+ +
+ +
- +
); } @@ -76,21 +113,21 @@ function BlogHeader({ description?: string; }) { return ( -
+
Back to Blog
-

+

{title}

{description && ( -

+

{description}

)} diff --git a/web/src/routes/blogs/index.tsx b/web/src/routes/blogs/index.tsx index 23d483f..3f0a032 100644 --- a/web/src/routes/blogs/index.tsx +++ b/web/src/routes/blogs/index.tsx @@ -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 ( - -
-

Blog

+ +
+

+ Resources +

+

+ Blog +

{posts.length === 0 ? ( -

+

No posts yet. Check back soon.

) : ( -
+
{posts.map((post) => ( -
+
-

+

{post.title}

{post.description && ( -

+

{post.description}

)} +

+ Read post +

))}
)} + +
+ +
- + ); } diff --git a/web/src/styles/app.css b/web/src/styles/app.css index da376a3..03ded05 100644 --- a/web/src/styles/app.css +++ b/web/src/styles/app.css @@ -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; }