Skip to content

Commit e5b6cac

Browse files
NiallJoeMaherclaude
andcommitted
feat: add /volunteer and /speakers forms with Google Sheets + email
Two public recruitment pages for the Codú community: - /volunteer — marketing/events volunteer sign-up - /speakers — pitch a talk (1–3 talks per submission, dynamic repeater) Both forms: - Submit via a tRPC mutation (volunteer.submit / speaker.submit) - Append a row to a shared "Codú Submissions" Google Sheet tab, and send an admin email notification in parallel via Promise.allSettled so a failure in one channel doesn't block the other - Include an invisible honeypot field to catch naive bots - Match the existing dark-theme Tailwind UI and use the ui-components primitives (Input, Textarea, Field, ErrorMessage) - Validate client-side with Zod (manual safeParse, matching the sponsor form pattern) and server-side via tRPC input schemas Google Sheets plumbing is generic — utils/googleSheets.ts exposes appendRowToSubmissionsSheet({ tab, values }) using a single service- account JWT, lazily instantiated and cached. Tab names live in config/submissions.ts so they're consistent across environments; only the sheet ID and service-account creds come from env. SEO: - Keyword-rich metadata + OG/Twitter cards on both pages - JobPosting JSON-LD (employmentType: VOLUNTEER) for richer SERP cards - /volunteer and /speakers added to sitemap Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ed3646f commit e5b6cac

18 files changed

Lines changed: 1869 additions & 18 deletions

app/(app)/speakers/_client.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { SpeakerForm } from "@/components/Speaker/SpeakerForm";
2+
3+
export function SpeakersClient() {
4+
return (
5+
<div className="bg-black">
6+
<div className="mx-auto max-w-3xl px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
7+
<header className="mb-10">
8+
<p className="mb-4 inline-block rounded-full bg-gradient-to-r from-orange-400/20 to-pink-600/20 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-orange-300">
9+
Speak at Codú
10+
</p>
11+
<h1 className="text-3xl font-extrabold tracking-tight text-white sm:text-4xl">
12+
Pitch a talk at a Codú meetup
13+
</h1>
14+
<p className="mt-4 text-lg leading-relaxed text-neutral-300">
15+
Codú runs regular meetups across Ireland and we&apos;re always
16+
looking for speakers. Whether it&apos;s your first talk or your
17+
fiftieth, we&apos;d love to hear your pitch. Propose a talk (or up
18+
to three) and we&apos;ll be in touch.
19+
</p>
20+
</header>
21+
22+
<SpeakerForm />
23+
24+
<p className="mt-6 text-center text-sm text-neutral-400">
25+
Takes about 3 minutes. First-time speakers welcome — we&apos;ll help
26+
you prep.
27+
</p>
28+
</div>
29+
</div>
30+
);
31+
}

app/(app)/speakers/page.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { Metadata } from "next";
2+
import { JsonLd } from "@/components/JsonLd";
3+
import { SpeakersClient } from "./_client";
4+
5+
const PAGE_URL = "https://www.codu.co/speakers";
6+
const PAGE_TITLE = "Speak at Codú — Pitch a Talk for Our Meetups";
7+
const PAGE_DESCRIPTION =
8+
"Pitch a talk at a Codú meetup. First-time speakers welcome. We run regular developer meetups across Ireland and are always looking for people to share what they've built, learned, or broken.";
9+
10+
export const metadata: Metadata = {
11+
title: PAGE_TITLE,
12+
description: PAGE_DESCRIPTION,
13+
keywords: [
14+
"Codú speaker",
15+
"tech meetup speaker Ireland",
16+
"developer meetup Dublin",
17+
"first time speaker",
18+
"web development talk",
19+
"speak at meetup Ireland",
20+
],
21+
alternates: { canonical: PAGE_URL },
22+
robots: { index: true, follow: true },
23+
openGraph: {
24+
title: "Speak at Codú",
25+
description: PAGE_DESCRIPTION,
26+
url: PAGE_URL,
27+
type: "website",
28+
},
29+
twitter: {
30+
card: "summary_large_image",
31+
title: "Speak at Codú",
32+
description: PAGE_DESCRIPTION,
33+
},
34+
};
35+
36+
const speakerJsonLd = {
37+
"@context": "https://schema.org",
38+
"@type": "JobPosting",
39+
title: "Speaker — Codú Meetups",
40+
description: PAGE_DESCRIPTION,
41+
employmentType: "VOLUNTEER",
42+
hiringOrganization: {
43+
"@type": "Organization",
44+
name: "Codú",
45+
sameAs: "https://www.codu.co",
46+
logo: "https://www.codu.co/images/codu-logo.png",
47+
},
48+
jobLocation: {
49+
"@type": "Place",
50+
address: {
51+
"@type": "PostalAddress",
52+
addressLocality: "Dublin",
53+
addressCountry: "IE",
54+
},
55+
},
56+
applicantLocationRequirements: { "@type": "Country", name: "Worldwide" },
57+
jobLocationType: "TELECOMMUTE",
58+
datePosted: new Date().toISOString().split("T")[0],
59+
url: PAGE_URL,
60+
};
61+
62+
export default function SpeakersPage() {
63+
return (
64+
<>
65+
<JsonLd data={speakerJsonLd} />
66+
<SpeakersClient />
67+
</>
68+
);
69+
}

app/(app)/volunteer/_client.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { VolunteerForm } from "@/components/Volunteer/VolunteerForm";
2+
3+
export function VolunteerClient() {
4+
return (
5+
<div className="bg-black">
6+
<div className="mx-auto max-w-3xl px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
7+
<header className="mb-10">
8+
<p className="mb-4 inline-block rounded-full bg-gradient-to-r from-orange-400/20 to-pink-600/20 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-orange-300">
9+
Volunteer with Codú
10+
</p>
11+
<h1 className="text-3xl font-extrabold tracking-tight text-white sm:text-4xl">
12+
Help us build Ireland&apos;s largest web dev community
13+
</h1>
14+
<p className="mt-4 text-lg leading-relaxed text-neutral-300">
15+
Codú is Ireland&apos;s largest web dev community — thousands of
16+
developers, regular meetups, and a newsletter across the Irish tech
17+
ecosystem. We&apos;re opening volunteer spots for people interested
18+
in marketing and events.
19+
</p>
20+
</header>
21+
22+
<VolunteerForm />
23+
24+
<p className="mt-6 text-center text-sm text-neutral-400">
25+
Takes about 3 minutes. We read every application and reply within 2
26+
weeks.
27+
</p>
28+
</div>
29+
</div>
30+
);
31+
}

app/(app)/volunteer/page.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { Metadata } from "next";
2+
import { JsonLd } from "@/components/JsonLd";
3+
import { VolunteerClient } from "./_client";
4+
5+
const PAGE_URL = "https://www.codu.co/volunteer";
6+
const PAGE_TITLE = "Volunteer with Codú — Help Build Ireland's Largest Dev Community";
7+
const PAGE_DESCRIPTION =
8+
"Join the team behind Codú. We're recruiting volunteer marketers and event organisers to help run meetups, newsletters, partnerships, and socials across the Irish tech ecosystem.";
9+
10+
export const metadata: Metadata = {
11+
title: PAGE_TITLE,
12+
description: PAGE_DESCRIPTION,
13+
keywords: [
14+
"Codú volunteer",
15+
"volunteer developer community",
16+
"Ireland tech community",
17+
"web developer volunteer",
18+
"tech meetup organiser Ireland",
19+
"marketing volunteer",
20+
"events volunteer",
21+
],
22+
alternates: { canonical: PAGE_URL },
23+
robots: { index: true, follow: true },
24+
openGraph: {
25+
title: "Volunteer with Codú",
26+
description: PAGE_DESCRIPTION,
27+
url: PAGE_URL,
28+
type: "website",
29+
},
30+
twitter: {
31+
card: "summary_large_image",
32+
title: "Volunteer with Codú",
33+
description: PAGE_DESCRIPTION,
34+
},
35+
};
36+
37+
const volunteerJsonLd = {
38+
"@context": "https://schema.org",
39+
"@type": "JobPosting",
40+
title: "Volunteer — Marketing & Events",
41+
description: PAGE_DESCRIPTION,
42+
employmentType: "VOLUNTEER",
43+
hiringOrganization: {
44+
"@type": "Organization",
45+
name: "Codú",
46+
sameAs: "https://www.codu.co",
47+
logo: "https://www.codu.co/images/codu-logo.png",
48+
},
49+
jobLocation: {
50+
"@type": "Place",
51+
address: {
52+
"@type": "PostalAddress",
53+
addressCountry: "IE",
54+
},
55+
},
56+
applicantLocationRequirements: { "@type": "Country", name: "Worldwide" },
57+
jobLocationType: "TELECOMMUTE",
58+
datePosted: new Date().toISOString().split("T")[0],
59+
url: PAGE_URL,
60+
};
61+
62+
export default function VolunteerPage() {
63+
return (
64+
<>
65+
<JsonLd data={volunteerJsonLd} />
66+
<VolunteerClient />
67+
</>
68+
);
69+
}

app/sitemap.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const ROUTES_TO_INDEX = [
1414
"/feed",
1515
"/advertise",
1616
"/code-of-conduct",
17+
"/volunteer",
18+
"/speakers",
1719
];
1820

1921
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {

0 commit comments

Comments
 (0)