Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Hero } from "@/components/hero/hero";
import { ProjectGrid } from "@/components/project/project-grid";
import { ProjectPreviewModal } from "@/components/project/project-preview-modal";
import { UpstreamFixModal } from "@/components/upstream/upstream-fix-modal";
import { UpstreamProofBand } from "@/components/home/upstream-proof-band";
import { OrganizationJsonLd } from "@/components/seo/json-ld";
import type { Locale } from "@/i18n/locales";
import { buildLocaleAlternates, canonicalFor, METADATA_BASE } from "@/lib/seo/alternates";
Expand Down Expand Up @@ -39,6 +40,7 @@ export default async function HomePage({
<>
<OrganizationJsonLd />
<Hero />
<UpstreamProofBand locale={locale} />
<ProjectGrid />
<ContactSection />
<ProjectPreviewModal />
Expand Down
18 changes: 11 additions & 7 deletions app/[locale]/process/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getTranslations, setRequestLocale } from "next-intl/server";
import { Link } from "@/i18n/navigation";
import type { Locale } from "@/i18n/locales";
import { buildLocaleAlternates, canonicalFor, METADATA_BASE } from "@/lib/seo/alternates";
import { BookACall } from "@/components/cta/book-a-call";

export async function generateMetadata({
params,
Expand Down Expand Up @@ -125,13 +126,16 @@ function CtaBlock() {
return (
<section className="border-fg/5 mx-auto max-w-5xl border-t px-6 py-16 md:py-24">
<div className="mx-auto flex max-w-2xl flex-col items-center text-center">
<Link
href={{ pathname: "/contact", query: { ref: "process" } }}
className="bg-accent hover:bg-accent-glow text-bg inline-flex items-center gap-2 rounded-full px-6 py-3 text-base font-medium transition-colors"
>
{t("primary")}
<ArrowRight size={16} />
</Link>
<div className="flex flex-col items-center gap-3 sm:flex-row">
<Link
href={{ pathname: "/contact", query: { ref: "process" } }}
className="bg-accent hover:bg-accent-glow text-bg inline-flex items-center gap-2 rounded-full px-6 py-3 text-base font-medium transition-colors"
>
{t("primary")}
<ArrowRight size={16} />
</Link>
<BookACall variant="ghost" className="px-6 py-3 text-base" />
</div>
<p className="text-fg-muted mt-5 text-sm">
{t.rich("sub", {
link: (chunks) => (
Expand Down
4 changes: 4 additions & 0 deletions components/contact/contact-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useTranslations } from "next-intl";
import { ContactForm } from "./contact-form";
import { BookACall } from "@/components/cta/book-a-call";

export function ContactSection() {
const t = useTranslations("home.contactSection");
Expand All @@ -26,6 +27,9 @@ export function ContactSection() {
<p className="text-fg-muted mx-auto mt-4 max-w-xl text-pretty md:text-lg">
{t("sub")}
</p>
<div className="mt-6 flex justify-center">
<BookACall variant="ghost" />
</div>
</div>

<div className="mt-12">
Expand Down
42 changes: 42 additions & 0 deletions components/cta/book-a-call.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ArrowUpRight } from "lucide-react";
import { useTranslations } from "next-intl";
import { cn } from "@/lib/cn";

/**
* Zero-friction "Book a call" CTA — links straight to the self-hosted
* Cal.com discovery event, bypassing the 5-step contact form.
*
* Renders only when `NEXT_PUBLIC_CAL_URL` is set. That var is baked into
* the client bundle at build time on the VPS (`.github/workflows/deploy.yml`)
* and is intentionally absent in local dev — so this no-ops locally and
* appears only in production, exactly like the contact-form success CTA.
*/
export function BookACall({
variant = "solid",
className,
}: {
variant?: "solid" | "ghost";
className?: string;
}) {
const t = useTranslations("common.actions");
const calUrl = process.env.NEXT_PUBLIC_CAL_URL;
if (!calUrl) return null;

return (
<a
href={calUrl}
target="_blank"
rel="noopener noreferrer"
className={cn(
"inline-flex items-center gap-2 rounded-full px-5 py-2.5 text-sm font-medium transition-colors",
variant === "solid"
? "bg-accent hover:bg-accent-glow text-bg"
: "border-fg/15 hover:border-accent text-fg-muted hover:text-fg border",
className,
)}
>
{t("bookCall")}
<ArrowUpRight size={14} />
</a>
);
}
98 changes: 98 additions & 0 deletions components/home/upstream-proof-band.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { getTranslations } from "next-intl/server";
import { ArrowRight } from "lucide-react";
import { Link } from "@/i18n/navigation";
import { aggregates, findRepo, type UpstreamRepo } from "@/lib/upstream";
import { BrandIcon } from "@/components/upstream/brand-icon";

/**
* Home trust band — promotes the /upstream OSS proof (merged PRs into the
* tools people's stacks actually run on) to the home page as a named,
* quantified credibility signal. Counts are data-driven off
* `aggregates()` so they never drift from the enriched snapshot.
*
* MARQUEE labels are brand display names — never translated (brand names
* stay verbatim per the i18n rules). Each slug maps to a repo in
* content/upstream.json; a slug with no match is skipped gracefully.
*/
const MARQUEE: { slug: string; label: string }[] = [
{ slug: "nodejs-undici", label: "Node.js" },
{ slug: "mozilla-pdf-js", label: "Mozilla" },
{ slug: "mongodb-js-bson", label: "MongoDB" },
{ slug: "redis-node-redis", label: "Redis" },
{ slug: "puppeteer-puppeteer", label: "Puppeteer" },
{ slug: "remix-run-react-router", label: "React Router" },
{ slug: "graphql-graphql-js", label: "GraphQL" },
{ slug: "eslint-eslint", label: "ESLint" },
{ slug: "solidjs-solid", label: "Solid" },
{ slug: "statelyai-xstate", label: "XState" },
{ slug: "mantinedev-mantine", label: "Mantine" },
{ slug: "vitest-dev-eslint-plugin-vitest", label: "Vitest" },
];

export async function UpstreamProofBand({ locale }: { locale: string }) {
const t = await getTranslations({ locale, namespace: "home.upstream" });
const agg = aggregates();

const brands = MARQUEE.map((m) => {
const repo = findRepo(m.slug);
return repo ? { ...m, repo } : null;
}).filter(
(b): b is { slug: string; label: string; repo: UpstreamRepo } => b !== null,
);

return (
<section className="relative border-t border-fg/5 px-6 py-20 md:py-28">
<div
aria-hidden
className="pointer-events-none absolute inset-0 -z-10"
style={{
background:
"radial-gradient(55% 60% at 85% 10%, oklch(0.65 0.25 295 / 0.06), transparent 70%)",
}}
/>
<div className="mx-auto max-w-6xl">
<p className="font-mono text-fg-subtle text-xs uppercase tracking-wider">
{t("eyebrow")}
</p>
<h2 className="font-display mt-3 max-w-3xl text-balance text-3xl font-semibold tracking-tight md:text-4xl">
{t("heading")}
</h2>
<p className="text-fg-muted mt-5 max-w-2xl text-pretty md:text-lg">
{t("sub", { prs: agg.totalPrs, repos: agg.totalRepos })}
</p>

<ul className="mt-12 grid grid-cols-3 gap-x-6 gap-y-8 sm:grid-cols-4 md:grid-cols-6">
{brands.map(({ slug, label, repo }) => (
<li
key={slug}
className="flex flex-col items-center gap-2 text-center"
>
<BrandIcon
iconSlug={repo.iconSlug}
fallbackName={label}
customLogoKey={repo.slug}
size={40}
/>
<span className="font-mono text-2xs uppercase tracking-wider text-fg-subtle">
{label}
</span>
</li>
))}
</ul>

<div className="mt-12">
<Link
href="/upstream"
className="group text-accent inline-flex items-center gap-1.5 text-sm font-medium"
>
{t("cta")}
<ArrowRight
size={15}
className="transition-transform group-hover:translate-x-1"
/>
</Link>
</div>
</div>
</section>
);
}
33 changes: 33 additions & 0 deletions components/nav/footer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { siGithub } from "simple-icons";
import type { SimpleIcon } from "simple-icons";
import { useTranslations } from "next-intl";
import { Link } from "@/i18n/navigation";

/**
* Public social profiles — kept in sync with the JSON-LD `sameAs` array in
* `components/seo/json-ld.tsx`. GitHub is live today; LinkedIn / X land here
* (and in `sameAs`) once those handles are confirmed. Brand marks come from
* `simple-icons` (lucide-react v1 dropped brand icons).
*/
const SOCIALS: { href: string; label: string; icon: SimpleIcon }[] = [
{ href: "https://github.com/spokodev", label: "GitHub", icon: siGithub },
];

export function Footer() {
const t = useTranslations("common.footer");
const nav = useTranslations("common.nav");
Expand All @@ -14,6 +26,27 @@ export function Footer() {
<div className="mx-auto flex max-w-6xl flex-col items-start justify-between gap-4 md:flex-row md:items-center">
<p className="text-pretty">{t("tagline")}</p>
<div className="flex items-center gap-6">
{SOCIALS.map(({ href, label, icon }) => (
<a
key={label}
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={`Spoko Studio on ${label}`}
className="hover:text-fg transition-colors"
>
<svg
role="img"
viewBox="0 0 24 24"
width={18}
height={18}
fill="currentColor"
aria-hidden
>
<path d={icon.path} />
</svg>
</a>
))}
<Link
href="/upstream"
className="hover:text-fg transition-colors"
Expand Down
8 changes: 6 additions & 2 deletions components/nav/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { Link } from "@/i18n/navigation";
import { LocaleAccordion } from "./locale-accordion";
import { LocaleSwitcher } from "./locale-switcher";

type NavItem = { href: "/work" | "/services" | "/upstream" | "/about"; label: string };
type NavItem = {
href: "/work" | "/services" | "/process" | "/upstream" | "/about";
label: string;
};

/**
* Top-of-page navigation. Above `md`, all links + LocaleSwitcher sit inline.
Expand All @@ -32,6 +35,7 @@ export function Header({ showUpstream }: { showUpstream: boolean }) {
const items: NavItem[] = [
{ href: "/work", label: t("work") },
{ href: "/services", label: t("services") },
{ href: "/process", label: t("process") },
...(showUpstream ? ([{ href: "/upstream", label: t("upstream") }] as const) : []),
{ href: "/about", label: t("about") },
];
Expand Down Expand Up @@ -142,7 +146,7 @@ export function Header({ showUpstream }: { showUpstream: boolean }) {
className="bg-accent/10 border-accent/40 text-fg hover:bg-accent/15 group flex min-h-16 items-center gap-4 rounded-2xl border px-4 transition-colors"
>
<span className="font-mono text-2xs uppercase tracking-widest text-accent">
04
{String(items.length + 1).padStart(2, "0")}
</span>
<span className="font-display flex-1 text-2xl font-medium">
{t("contact")}
Expand Down
3 changes: 3 additions & 0 deletions components/seo/json-ld.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,11 @@ export function OrganizationJsonLd() {
"@id": `${SITE_URL}/#founder`,
name: "Yaroslav",
jobTitle: "Founder & Principal Engineer",
description:
"Principal engineer with fourteen years across the stack — sysadmin, QA, product, and engineering — leading Spoko Studio.",
url: `${SITE_URL}/about`,
knowsAbout: EXPERTISE,
sameAs: ["https://github.com/spokodev"],
},
sameAs: ["https://github.com/spokodev"],
address: {
Expand Down
6 changes: 3 additions & 3 deletions messages/de/about.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
},
"whatWeDo": {
"eyebrow": "Wie wir aufgestellt sind",
"heading": "Ein Ingenieur im Kern. Ein Netzwerk drum herum.",
"p1": "Im Herzen von Spoko Studio steht ein Ingenieur mit vierzehn Jahren Erfahrung, der persönlich in jedes Projekt investiert ist, das wir ausliefern. Für größere Vorhaben arbeiten wir mit einem bewährten Netzwerk von Spezialisten zusammen, mit denen wir seit Jahren echte Produkte gebaut haben.",
"p2": "Wir wählen Arbeit, in der wir monatelang wirklich aufgehen wollen. Wir launchen in Wochen, nicht in Quartalen. Und wir bleiben in den Jahren danach — dieselbe Codebasis, weiterhin im Wachstum. Die Projekte auf dieser Seite beantworten «Was machen Sie eigentlich?» besser als jede Biografie."
"p2": "Wir wählen Arbeit, in der wir monatelang wirklich aufgehen wollen. Wir launchen in Wochen, nicht in Quartalen. Und wir bleiben in den Jahren danach — dieselbe Codebasis, weiterhin im Wachstum. Die Projekte auf dieser Seite beantworten «Was machen Sie eigentlich?» besser als jede Biografie.",
"heading": "Im Kern ein Ingenieur. Um ihn herum ein Team.",
"p1": "Spoko Studio wird von Yaroslav geleitet – einem leitenden Ingenieur mit vierzehn Jahren Erfahrung in allen Bereichen, von Systemadministration und Qualitätssicherung bis hin zu Produktentwicklung und Technik. Er engagiert sich persönlich für jedes Projekt, das wir auf den Markt bringen. Um ihn herum arbeitet ein kleines, eingespieltes Team aus Ingenieuren und Spezialisten, mit denen wir seit Jahren echte Produkte entwickeln."
},
"process": {
"eyebrow": "Wie wir arbeiten",
Expand Down
6 changes: 4 additions & 2 deletions messages/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
},
"primaryNav": "Hauptnavigation",
"openMenu": "Menü öffnen",
"closeMenu": "Menü schließen"
"closeMenu": "Menü schließen",
"process": "Prozess"
},
"footer": {
"tagline": "Mit Sorgfalt gebaut – von Menschen, für Menschen.",
Expand Down Expand Up @@ -45,6 +46,7 @@
"velocityLaunchedLabel": "Gelauncht in",
"velocityLiveLabel": "Seit … live",
"breadcrumbHome": "Startseite",
"breadcrumbWork": "Projekte"
"breadcrumbWork": "Projekte",
"bookCall": "Buchen Sie ein 30-minütiges Telefonat"
}
}
10 changes: 8 additions & 2 deletions messages/de/home.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
"hero": {
"h1Line1": "Sie sehen die Idee.",
"h1Line2": "Wir sehen den Weg dorthin.",
"sub": "Spoko Studio ist ein Engineer plus ein erweitertes Netzwerk, das die Dinge, von denen Sie geträumt haben — die Sie aber noch nicht umsetzen konnten — in fertige Produkte verwandelt, die im Betrieb zuverlässig laufen.",
"ctaPrimary": "Unsere Projekte entdecken",
"ctaSecondary": "Oder sprechen wir"
"ctaSecondary": "Oder sprechen wir",
"sub": "Spoko Studio ist ein kleines Team von Entwicklern, das Ihre Träume – die Sie bisher noch nicht verwirklichen konnten – in marktreife Produkte verwandelt, die im Produktivbetrieb zuverlässig funktionieren."
},
"scrollCue": "Scrollen",
"selectedWork": {
Expand All @@ -19,5 +19,11 @@
"eyebrow": "Kontakt",
"heading": "Eine Idee, die sich noch nicht ganz in Worte fassen lässt?",
"sub": "Genau dann sind wir hilfreich. Sagen Sie uns, was Sie sehen — wir helfen, den Weg zu finden."
},
"upstream": {
"eyebrow": "Open Source",
"heading": "Wir kümmern uns um die Tools, auf denen Ihr Stack läuft.",
"sub": "{prs} zusammengeführte Pull-Anfragen in {repos} Open-Source-Projekten – Node.js, MongoDB, Redis, Mozilla und weiteren. Keine Demos: echte Fehler, die reproduziert, behoben und in den Upstream integriert wurden.",
"cta": "Entdecken Sie die vorgelagerten Arbeiten"
}
}
4 changes: 2 additions & 2 deletions messages/en/about.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
},
"whatWeDo": {
"eyebrow": "How we're built",
"heading": "An engineer at the heart. A network around them.",
"p1": "At the heart of Spoko Studio is an engineer with fourteen years of craft, personally invested in every project we ship. For larger scopes, we collaborate with a trusted network of specialists we've built real products with for years.",
"heading": "An engineer at the heart. A team around them.",
"p1": "Spoko Studio is led by Yaroslav — a principal engineer with fourteen years across the stack, from sysadmin and QA to product and engineering. He's personally invested in every project we ship. Around him works a small, trusted team of engineers and specialists we've built real products with for years.",
"p2": "We choose work we want to live inside for months at a time. We launch in weeks, not quarters. And we stay around for the years that follow — same codebase, still evolving year over year. The projects on this site answer \"what do you do?\" better than any biography."
},
"process": {
Expand Down
4 changes: 3 additions & 1 deletion messages/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"nav": {
"work": "Work",
"services": "Services",
"process": "Process",
"upstream": "Upstream",
"about": "About",
"contact": "Contact",
Expand Down Expand Up @@ -45,6 +46,7 @@
"velocityLaunchedLabel": "Launched in",
"velocityLiveLabel": "Live for",
"breadcrumbHome": "Home",
"breadcrumbWork": "Work"
"breadcrumbWork": "Work",
"bookCall": "Book a 30-min call"
}
}
8 changes: 7 additions & 1 deletion messages/en/home.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@
"hero": {
"h1Line1": "You see the idea.",
"h1Line2": "We see the path to it.",
"sub": "Spoko Studio is one engineer plus an extended network that turns the things you've been dreaming about — but couldn't yet build — into launched products that keep working in production.",
"sub": "Spoko Studio is a small team of engineers who turn the things you've been dreaming about — but couldn't yet build — into launched products that keep working in production.",
"ctaPrimary": "Explore our work",
"ctaSecondary": "Or let's talk"
},
"scrollCue": "Scroll",
"upstream": {
"eyebrow": "Open source",
"heading": "We fix the tools your stack runs on.",
"sub": "{prs} merged pull requests across {repos} open-source projects — Node.js, MongoDB, Redis, Mozilla and more. Not demos: real bugs, reproduced, fixed, and merged upstream.",
"cta": "Explore the upstream work"
},
"selectedWork": {
"eyebrow": "Selected work",
"heading": "Seven projects, launched and live."
Expand Down
Loading
Loading