Add public support page (#518)

This commit is contained in:
Ben Senescu 2026-08-22 11:27:15 -04:00 committed by Ben Senescu
parent 4ba2d6c175
commit d403954323
4 changed files with 186 additions and 0 deletions

View File

@ -29,6 +29,7 @@ const STATIC_PATHS = [
"/open-source-seo",
"/google-search-console-mcp",
"/roadmap",
"/support",
...Object.values(FEATURE_PAGE_SLUGS).map((slug) => `/features/${slug}`),
];

View File

@ -60,6 +60,7 @@ export function SiteFooter({ className }: { className?: string }) {
<div>
<p className="font-semibold text-neutral-900">Company</p>
<div className="mt-2 flex flex-col gap-1.5">
<Link to="/support">Support</Link>
<Link to="/roadmap">Roadmap</Link>
<Link to="/pricing">Pricing</Link>
<a

View File

@ -23,6 +23,7 @@ import { Route as BlogsSplatRouteImport } from './routes/blogs/$'
import { Route as ApiSubscribeRouteImport } from './routes/api/subscribe'
import { Route as ApiEventRouteImport } from './routes/api/event'
import { Route as ApiBacklinkCheckRouteImport } from './routes/api/backlink-check'
import { Route as MarketingSupportRouteImport } from './routes/_marketing/support'
import { Route as MarketingRoadmapRouteImport } from './routes/_marketing/roadmap'
import { Route as MarketingPricingRouteImport } from './routes/_marketing/pricing'
import { Route as MarketingOpenSourceSeoRouteImport } from './routes/_marketing/open-source-seo'
@ -124,6 +125,11 @@ const ApiBacklinkCheckRoute = ApiBacklinkCheckRouteImport.update({
path: '/api/backlink-check',
getParentRoute: () => rootRouteImport,
} as any)
const MarketingSupportRoute = MarketingSupportRouteImport.update({
id: '/support',
path: '/support',
getParentRoute: () => MarketingRoute,
} as any)
const MarketingRoadmapRoute = MarketingRoadmapRouteImport.update({
id: '/roadmap',
path: '/roadmap',
@ -318,6 +324,7 @@ export interface FileRoutesByFullPath {
'/open-source-seo': typeof MarketingOpenSourceSeoRoute
'/pricing': typeof MarketingPricingRoute
'/roadmap': typeof MarketingRoadmapRoute
'/support': typeof MarketingSupportRoute
'/api/backlink-check': typeof ApiBacklinkCheckRoute
'/api/event': typeof ApiEventRoute
'/api/subscribe': typeof ApiSubscribeRoute
@ -363,6 +370,7 @@ export interface FileRoutesByTo {
'/open-source-seo': typeof MarketingOpenSourceSeoRoute
'/pricing': typeof MarketingPricingRoute
'/roadmap': typeof MarketingRoadmapRoute
'/support': typeof MarketingSupportRoute
'/api/backlink-check': typeof ApiBacklinkCheckRoute
'/api/event': typeof ApiEventRoute
'/api/subscribe': typeof ApiSubscribeRoute
@ -411,6 +419,7 @@ export interface FileRoutesById {
'/_marketing/open-source-seo': typeof MarketingOpenSourceSeoRoute
'/_marketing/pricing': typeof MarketingPricingRoute
'/_marketing/roadmap': typeof MarketingRoadmapRoute
'/_marketing/support': typeof MarketingSupportRoute
'/api/backlink-check': typeof ApiBacklinkCheckRoute
'/api/event': typeof ApiEventRoute
'/api/subscribe': typeof ApiSubscribeRoute
@ -460,6 +469,7 @@ export interface FileRouteTypes {
| '/open-source-seo'
| '/pricing'
| '/roadmap'
| '/support'
| '/api/backlink-check'
| '/api/event'
| '/api/subscribe'
@ -505,6 +515,7 @@ export interface FileRouteTypes {
| '/open-source-seo'
| '/pricing'
| '/roadmap'
| '/support'
| '/api/backlink-check'
| '/api/event'
| '/api/subscribe'
@ -552,6 +563,7 @@ export interface FileRouteTypes {
| '/_marketing/open-source-seo'
| '/_marketing/pricing'
| '/_marketing/roadmap'
| '/_marketing/support'
| '/api/backlink-check'
| '/api/event'
| '/api/subscribe'
@ -707,6 +719,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ApiBacklinkCheckRouteImport
parentRoute: typeof rootRouteImport
}
'/_marketing/support': {
id: '/_marketing/support'
path: '/support'
fullPath: '/support'
preLoaderRoute: typeof MarketingSupportRouteImport
parentRoute: typeof MarketingRoute
}
'/_marketing/roadmap': {
id: '/_marketing/roadmap'
path: '/roadmap'
@ -933,6 +952,7 @@ interface MarketingRouteChildren {
MarketingOpenSourceSeoRoute: typeof MarketingOpenSourceSeoRoute
MarketingPricingRoute: typeof MarketingPricingRoute
MarketingRoadmapRoute: typeof MarketingRoadmapRoute
MarketingSupportRoute: typeof MarketingSupportRoute
MarketingIndexRoute: typeof MarketingIndexRoute
MarketingFeaturesAiBrandVisibilityRoute: typeof MarketingFeaturesAiBrandVisibilityRoute
MarketingFeaturesAiSearchPromptsRoute: typeof MarketingFeaturesAiSearchPromptsRoute
@ -968,6 +988,7 @@ const MarketingRouteChildren: MarketingRouteChildren = {
MarketingOpenSourceSeoRoute: MarketingOpenSourceSeoRoute,
MarketingPricingRoute: MarketingPricingRoute,
MarketingRoadmapRoute: MarketingRoadmapRoute,
MarketingSupportRoute: MarketingSupportRoute,
MarketingIndexRoute: MarketingIndexRoute,
MarketingFeaturesAiBrandVisibilityRoute:
MarketingFeaturesAiBrandVisibilityRoute,

View File

@ -0,0 +1,163 @@
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { buildPageSeo } from "@/lib/seo";
const SUPPORT_EMAIL = "ben@openseo.so";
const DISCORD_URL = "https://discord.gg/c9uGs3cFXr";
const GITHUB_ISSUES_URL = "https://github.com/every-app/open-seo/issues";
export const Route = createFileRoute("/_marketing/support")({
head: () =>
buildPageSeo({
title: "Support",
description:
"Get help with OpenSEO, share feedback, or report an issue by email, Discord, or GitHub.",
path: "/support",
titleSuffix: "OpenSEO",
}),
component: SupportPage,
});
function SupportPage() {
const [copied, setCopied] = useState(false);
async function copyEmail() {
await navigator.clipboard.writeText(SUPPORT_EMAIL);
setCopied(true);
window.setTimeout(() => setCopied(false), 2000);
}
return (
<article className="mx-auto max-w-4xl">
<header className="max-w-3xl">
<p className="text-sm font-medium text-[var(--color-brand-accent)]">
Help &amp; Community
</p>
<h1 className="mt-3 text-4xl font-semibold leading-tight tracking-tight text-neutral-950 md:text-6xl">
We want to hear from you
</h1>
<p className="mt-5 max-w-2xl text-lg leading-8 text-[var(--color-brand-muted)]">
We want to talk to you! We&apos;re super open to feedback and want to
learn how you work so we can make OpenSEO better.
</p>
</header>
<div className="mt-12 grid gap-4 md:grid-cols-3">
<section className="flex min-h-64 flex-col rounded-xl border border-[var(--color-border-subtle)] bg-white p-6">
<p className="font-mono text-xs text-[var(--color-brand-accent)]">
01
</p>
<h2 className="mt-6 text-xl font-semibold tracking-tight text-neutral-950">
Email
</h2>
<p className="mt-2 text-sm leading-6 text-[var(--color-brand-muted)]">
Send ideas, problems, questions, or feedback directly.
</p>
<button
type="button"
onClick={copyEmail}
aria-live="polite"
className="mt-auto inline-flex w-fit items-center gap-2 pt-6 text-sm font-medium text-neutral-950 transition-colors hover:text-[var(--color-brand-accent)]"
>
<span className="font-mono text-xs">{SUPPORT_EMAIL}</span>
{copied ? <CheckIcon /> : <CopyIcon />}
<span className="sr-only">{copied ? "Copied" : "Copy email"}</span>
</button>
</section>
<SupportCard
number="02"
title="Discord"
description="Ask for help, share ideas and learn from the community."
href={DISCORD_URL}
linkText="Join the Discord"
/>
<SupportCard
number="03"
title="GitHub Issues"
description="Report bugs or request features on GitHub."
href={GITHUB_ISSUES_URL}
linkText="Open an issue"
/>
</div>
</article>
);
}
function SupportCard({
number,
title,
description,
href,
linkText,
}: {
number: string;
title: string;
description: string;
href: string;
linkText: string;
}) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="group flex min-h-64 flex-col rounded-xl border border-[var(--color-border-subtle)] bg-white p-6 transition-all hover:-translate-y-0.5 hover:border-neutral-950 hover:shadow-lg hover:shadow-neutral-900/5"
>
<p className="font-mono text-xs text-[var(--color-brand-accent)]">
{number}
</p>
<h2 className="mt-6 text-xl font-semibold tracking-tight text-neutral-950">
{title}
</h2>
<p className="mt-2 text-sm leading-6 text-[var(--color-brand-muted)]">
{description}
</p>
<span className="mt-auto inline-flex items-center gap-2 pt-6 text-sm font-medium text-neutral-950">
{linkText}
<span
aria-hidden="true"
className="transition-transform group-hover:translate-x-1"
>
&rarr;
</span>
</span>
</a>
);
}
function CopyIcon() {
return (
<svg
aria-hidden="true"
className="h-3.5 w-3.5 text-neutral-400"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
);
}
function CheckIcon() {
return (
<svg
aria-hidden="true"
className="h-3.5 w-3.5 text-[var(--color-brand-accent)]"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m20 6-11 11-5-5" />
</svg>
);
}