diff --git a/public/images/pages/conduct.jpg b/public/images/pages/conduct.jpg new file mode 100644 index 0000000..e7e8d5f Binary files /dev/null and b/public/images/pages/conduct.jpg differ diff --git a/public/images/pages/current-speakers.jpg b/public/images/pages/current-speakers.jpg new file mode 100644 index 0000000..57619a0 Binary files /dev/null and b/public/images/pages/current-speakers.jpg differ diff --git a/public/images/pages/home.jpg b/public/images/pages/home.jpg new file mode 100644 index 0000000..4f47e17 Binary files /dev/null and b/public/images/pages/home.jpg differ diff --git a/public/images/pages/parners.jpg b/public/images/pages/parners.jpg new file mode 100644 index 0000000..351a290 Binary files /dev/null and b/public/images/pages/parners.jpg differ diff --git a/public/images/pages/past-speakers.jpg b/public/images/pages/past-speakers.jpg new file mode 100644 index 0000000..fdaf8c1 Binary files /dev/null and b/public/images/pages/past-speakers.jpg differ diff --git a/public/images/pages/privacy.jpg b/public/images/pages/privacy.jpg new file mode 100644 index 0000000..41be8bc Binary files /dev/null and b/public/images/pages/privacy.jpg differ diff --git a/public/images/pages/schedule.jpg b/public/images/pages/schedule.jpg new file mode 100644 index 0000000..365f6b8 Binary files /dev/null and b/public/images/pages/schedule.jpg differ diff --git a/public/images/pages/sponsors.jpg b/public/images/pages/sponsors.jpg new file mode 100644 index 0000000..8350b69 Binary files /dev/null and b/public/images/pages/sponsors.jpg differ diff --git a/public/images/pages/team.jpg b/public/images/pages/team.jpg new file mode 100644 index 0000000..7409063 Binary files /dev/null and b/public/images/pages/team.jpg differ diff --git a/src/app/conduct/page.tsx b/src/app/conduct/page.tsx index 5f067e7..db4a9e4 100644 --- a/src/app/conduct/page.tsx +++ b/src/app/conduct/page.tsx @@ -1,3 +1,13 @@ +import { createMetadata } from "@/lib/seo"; + +export const metadata = createMetadata({ + title: "Code of Conduct", + description: + "SINFO is dedicated to providing a harassment-free conference experience for everyone. Read our Code of Conduct.", + path: "/conduct", + image: "/images/pages/conduct.jpg", +}); + export default function CodeConduct() { return (
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4ab6f68..07468a9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -9,9 +9,65 @@ import { EventService } from "@/services/EventService"; const montserrat = Montserrat({ subsets: ["latin"] }); +const siteUrl = "https://sinfo.org"; + export const metadata: Metadata = { - title: "SINFO Website", - description: "SINFO Website", + metadataBase: new URL(siteUrl), + title: { + default: "SINFO — Portugal's Biggest Free Tech Conference", + template: "%s | SINFO", + }, + description: + "SINFO is Portugal's biggest free technology conference, held annually at Instituto Superior Técnico in Lisbon. Join thousands of tech enthusiasts, industry leaders, and innovators.", + keywords: [ + "SINFO", + "tech conference", + "Portugal", + "Lisbon", + "IST", + "technology", + "speakers", + "innovation", + "free conference", + ], + authors: [{ name: "SINFO", url: siteUrl }], + creator: "SINFO", + openGraph: { + type: "website", + locale: "en_US", + url: siteUrl, + siteName: "SINFO", + title: "SINFO — Portugal's Biggest Free Tech Conference", + description: + "SINFO is Portugal's biggest free technology conference, held annually at Instituto Superior Técnico in Lisbon. Join thousands of tech enthusiasts, industry leaders, and innovators.", + images: [ + { + url: "/images/pages/home.jpg", + alt: "SINFO — Portugal's Biggest Free Tech Conference", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "SINFO — Portugal's Biggest Free Tech Conference", + description: + "SINFO is Portugal's biggest free technology conference, held annually at Instituto Superior Técnico in Lisbon.", + images: ["/images/pages/home.jpg"], + creator: "@sinfosl", + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + "max-image-preview": "large", + "max-snippet": -1, + }, + }, + alternates: { + canonical: siteUrl, + }, }; export const viewport: Viewport = { diff --git a/src/app/manifest.ts b/src/app/manifest.ts index bb58037..aafdda9 100644 --- a/src/app/manifest.ts +++ b/src/app/manifest.ts @@ -2,9 +2,10 @@ import type { MetadataRoute } from "next"; export default function manifest(): MetadataRoute.Manifest { return { - name: "SINFO - Website", + name: "SINFO — Portugal's Biggest Free Tech Conference", short_name: "SINFO", - description: "SINFO Website", + description: + "SINFO is Portugal's biggest free technology conference, held annually at Instituto Superior Técnico in Lisbon.", start_url: "/", display: "standalone", background_color: "#1c2b70", // SINFO Primary diff --git a/src/app/partners/page.tsx b/src/app/partners/page.tsx index 406ecf4..b0ea9cb 100644 --- a/src/app/partners/page.tsx +++ b/src/app/partners/page.tsx @@ -1,4 +1,13 @@ +import { createMetadata } from "@/lib/seo"; import { CompanyService } from "@/services/CompanyService"; + +export const metadata = createMetadata({ + title: "Partners", + description: + "Meet the organisations and partners that collaborate with SINFO to deliver an unforgettable tech conference experience.", + path: "/partners", + image: "/images/pages/parners.jpg", +}); import { EventService } from "@/services/EventService"; import BlankPageMessage from "@/components/BlankPageMessage"; import GridList from "@/components/GridList"; diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx index 6861982..46b4130 100644 --- a/src/app/privacy/page.tsx +++ b/src/app/privacy/page.tsx @@ -1,3 +1,13 @@ +import { createMetadata } from "@/lib/seo"; + +export const metadata = createMetadata({ + title: "Privacy Policy", + description: + "Read SINFO's Privacy Policy to understand how we collect, use, and protect your personal information.", + path: "/privacy", + image: "/images/pages/privacy.jpg", +}); + export default function PrivacyPage() { return (
diff --git a/src/app/schedule/page.tsx b/src/app/schedule/page.tsx index 0b57af0..cd03a02 100644 --- a/src/app/schedule/page.tsx +++ b/src/app/schedule/page.tsx @@ -1,4 +1,13 @@ +import { createMetadata } from "@/lib/seo"; import Link from "next/link"; + +export const metadata = createMetadata({ + title: "Schedule", + description: + "Check out the SINFO event schedule — talks, workshops, panels, and more.", + path: "/schedule", + image: "/images/pages/schedule.jpg", +}); import ImageWithFallback from "@/components/ImageWithFallback"; import { workHacky } from "@/assets/images"; import CallToAction from "@/components/CallToAction"; diff --git a/src/app/speakers/[id]/page.tsx b/src/app/speakers/[id]/page.tsx index d6da746..a0c963f 100644 --- a/src/app/speakers/[id]/page.tsx +++ b/src/app/speakers/[id]/page.tsx @@ -1,4 +1,5 @@ import React from "react"; +import type { Metadata } from "next"; import Link from "next/link"; import { SpeakerService } from "@/services/SpeakerService"; import { SessionService } from "@/services/SessionService"; @@ -6,6 +7,7 @@ import { generateTimeInterval } from "@/utils/utils"; import ImageWithFallback from "@/components/ImageWithFallback"; import { ShowMore } from "@/components/ShowMore"; import { Calendar, Clock, MapPin } from "lucide-react"; +import { createMetadata } from "@/lib/seo"; export const dynamic = "force-dynamic"; @@ -15,6 +17,28 @@ type Props = { }; }; +export async function generateMetadata({ params }: Props): Promise { + const speaker = await SpeakerService.getSpeaker(params.id); + if (!speaker) return { title: "Speaker Not Found" }; + + const description = speaker.description + ? speaker.description.slice(0, 160) + : `${speaker.name} — speaker at SINFO, Portugal's biggest free tech conference.`; + + const seoImageUrl = `https://static.sinfo.org/website/33-sinfo/seo/speakers/${params.id}.jpg`; + const seoImageExists = await fetch(seoImageUrl, { method: "HEAD" }) + .then((r) => r.ok) + .catch(() => false); + const image = seoImageExists ? seoImageUrl : "/images/pages/home.jpg"; + + return createMetadata({ + title: speaker.name, + description, + path: `/speakers/${params.id}`, + image, + }); +} + export default async function Page({ params }: Props) { const { id } = params; const speaker = await SpeakerService.getSpeaker(id); diff --git a/src/app/speakers/page.tsx b/src/app/speakers/page.tsx index ba64114..0b01c23 100644 --- a/src/app/speakers/page.tsx +++ b/src/app/speakers/page.tsx @@ -1,5 +1,14 @@ import React from "react"; +import { createMetadata } from "@/lib/seo"; import BlankPageMessage from "@/components/BlankPageMessage"; + +export const metadata = createMetadata({ + title: "Speakers", + description: + "Meet the world-class speakers at SINFO — influential minds in technology and innovation shaping the future.", + path: "/speakers", + image: "/images/pages/current-speakers.jpg", +}); import SpeakerCard from "@/components/Home/CurrentSpeakersHighlight/SpeakerCard"; import { SpeakerService } from "@/services/SpeakerService"; import { EventService } from "@/services/EventService"; diff --git a/src/app/speakers/previous/page.tsx b/src/app/speakers/previous/page.tsx index 1e89f1a..286d288 100644 --- a/src/app/speakers/previous/page.tsx +++ b/src/app/speakers/previous/page.tsx @@ -1,5 +1,14 @@ import React from "react"; +import { createMetadata } from "@/lib/seo"; import BlankPageMessage from "@/components/BlankPageMessage"; + +export const metadata = createMetadata({ + title: "Past Speakers", + description: + "Explore the past speakers of SINFO — global voices that helped shape Portugal's biggest free tech conference.", + path: "/speakers/previous", + image: "/images/pages/past-speakers.jpg", +}); import SpeakerCard from "@/components/Home/CurrentSpeakersHighlight/SpeakerCard"; import { SpeakerService } from "@/services/SpeakerService"; import { buildEditionColorMap } from "@/utils/speakerColors"; diff --git a/src/app/sponsors/page.tsx b/src/app/sponsors/page.tsx index 6e62345..ff4f8ab 100644 --- a/src/app/sponsors/page.tsx +++ b/src/app/sponsors/page.tsx @@ -1,4 +1,13 @@ +import { createMetadata } from "@/lib/seo"; import { CompanyService } from "@/services/CompanyService"; + +export const metadata = createMetadata({ + title: "Sponsors", + description: + "Meet the amazing sponsors that make SINFO possible — companies committed to delivering a free world-class tech conference.", + path: "/sponsors", + image: "/images/pages/sponsors.jpg", +}); import { EventService } from "@/services/EventService"; import BlankPageMessage from "@/components/BlankPageMessage"; import GridList from "@/components/GridList"; diff --git a/src/app/team/page.tsx b/src/app/team/page.tsx index 96f7d8c..9839b69 100644 --- a/src/app/team/page.tsx +++ b/src/app/team/page.tsx @@ -1,4 +1,13 @@ +import { createMetadata } from "@/lib/seo"; import { MemberService } from "@/services/MemberService"; + +export const metadata = createMetadata({ + title: "Team", + description: + "Meet the passionate students behind SINFO — the team that organises Portugal's biggest free tech conference.", + path: "/team", + image: "/images/pages/team.jpg", +}); import { EventService } from "@/services/EventService"; import MemberCard from "@/components/MemberCard"; import BlankPageMessage from "@/components/BlankPageMessage"; diff --git a/src/lib/seo.ts b/src/lib/seo.ts new file mode 100644 index 0000000..24b2807 --- /dev/null +++ b/src/lib/seo.ts @@ -0,0 +1,48 @@ +import type { Metadata } from "next"; +import { headers } from "next/headers"; + +const defaultImage = "/images/pages/home.jpg"; + +function getSiteUrl(): string { + try { + const host = headers().get("host") ?? "sinfo.org"; + const proto = host.startsWith("localhost") ? "http" : "https"; + return `${proto}://${host}`; + } catch { + return "https://sinfo.org"; + } +} + +export function createMetadata({ + title, + description, + path = "", + image = defaultImage, +}: { + title: string; + description: string; + path?: string; + image?: string; +}): Metadata { + const siteUrl = getSiteUrl(); + const url = `${siteUrl}${path}`; + const absoluteImage = image.startsWith("http") ? image : `${siteUrl}${image}`; + + return { + title, + description, + openGraph: { + title: `${title} | SINFO`, + description, + url, + images: [{ url: absoluteImage, alt: title }], + }, + twitter: { + card: "summary_large_image", + title: `${title} | SINFO`, + description, + images: [absoluteImage], + }, + alternates: { canonical: url }, + }; +}