add certifications route
Build and Publish Docker Image / build (push) Successful in 14s
Build and Publish Docker Image / deploy (push) Successful in 2s

This commit is contained in:
2026-09-29 03:44:02 -04:00
parent 7c37a72a5e
commit 5c37015702
8 changed files with 274 additions and 9 deletions
+2 -4
View File
@@ -1,5 +1,4 @@
import { Link } from "react-router-dom";
import EmailLink from "./EmailLink";
import { PROFILES } from "../utils/site";
export default function Header() {
@@ -18,8 +17,8 @@ export default function Header() {
<Link to="/projects" className="hover:underline">
Projects
</Link>
<Link to="/sports" className="hover:underline">
Sports
<Link to="/certifications" className="hover:underline">
Certifications
</Link>
<a
href={PROFILES.github}
@@ -32,7 +31,6 @@ export default function Header() {
<a href={PROFILES.blog} rel="me" className="hover:underline">
Blog
</a>
<EmailLink className="hover:underline" />
</nav>
</header>
);
+16 -2
View File
@@ -14,11 +14,25 @@ export default function ProjectCard({ project }: { project: Project }) {
<div className="bg-neutral-800 rounded-lg p-2 border border-white">
<div className="flex items-start justify-between gap-3">
<h3 className="text-base font-bold">
<Link to={`/projects/${project.slug}`} className="hover:underline">
<Link
to={
!project.isCert
? `/projects/${project.slug}`
: `/certifications/${project.slug}`
}
className="hover:underline"
>
{project.title}
</Link>
</h3>
<Link to={`/projects/${project.slug}`} className="shrink-0">
<Link
to={
!project.isCert
? `/projects/${project.slug}`
: `/certifications/${project.slug}`
}
className="shrink-0"
>
<button className="bg-gray-500 border border-zinc-500 rounded-lg px-2 py-1 text-sm whitespace-nowrap">
View &gt;
</button>
+31
View File
@@ -8,6 +8,7 @@ export type Project = {
pills: string[];
/** paragraphs of prose for the detail page */
body: string[];
isCert?: boolean;
};
export const PROJECTS: Project[] = [
@@ -65,6 +66,36 @@ export const PROJECTS: Project[] = [
},
];
export const CERTIFICATIONS: Project[] = [
{
slug: "foundational-csharp",
title: "Foundational C# (Microsoft)",
description:
"Foundational C# course created by Microsoft, issued by FreeCodeCamp",
link: "https://www.freecodecamp.org/certification/alexavf/foundational-c-sharp-with-microsoft",
pills: ["Software Development", "C#"],
body: [
"A course created by Microsoft, a comprehensive intro to the core concepts and the syntax of C#.",
"Worked with creating basic, logical console applications and how to work with data and properly debug using Visual Studio.",
],
},
{
slug: "introduction-to-frontend-development",
title: "Intro to Frontend Development (Meta)",
description: "Intro to frontend created by Meta, issued by Coursera",
link: "https://coursera.org/share/df3426a13f7bcdaa9f82f88e77554425",
pills: ["Software Development", "JavaScript", "HTML", "CSS"],
body: [
"A course that took me through the basics of frontend development with JS, CSS, HTML and more.",
"Currently working on the full certification from Meta, including experience with React, Git, Figma and more.",
],
},
];
export function findProject(slug: string | undefined) {
return PROJECTS.find((p) => p.slug === slug);
}
export function findCertification(slug: string | undefined) {
return CERTIFICATIONS.find((p) => p.slug === slug);
}
+66
View File
@@ -0,0 +1,66 @@
import { Link, useParams } from "react-router-dom";
import Footer from "../components/Footer";
import NotFound from "./NotFound";
import { findCertification } from "../data/projects";
export default function CertificationDetail() {
const { slug } = useParams();
const cert = findCertification(slug);
if (!cert) return <NotFound />;
return (
<main className="my-12 sm:my-20">
<nav aria-label="Breadcrumb" className="py-2">
<p className="opacity-85 text-sm text-left">
<Link to="/certifications" className="hover:underline">
CERTIFICATIONS
</Link>
</p>
</nav>
<div className="py-2">
<h1 className="text-3xl sm:text-4xl text-balance py-2">{cert.title}</h1>
<div className="flex flex-wrap gap-2 py-2">
{cert.pills.map((pill) => (
<div
key={pill}
className="bg-zinc-500 rounded-lg text-xs p-1 font-semibold"
>
{pill}
</div>
))}
</div>
</div>
<div className="py-2 space-y-6">
{cert.body.map((paragraph, i) => (
<p key={i}>{paragraph}</p>
))}
</div>
<div className="py-4">
<a
href={cert.link}
target="_blank"
rel="noopener noreferrer"
className="text-lime-300 hover:underline"
>
View the certificate &gt;
</a>
</div>
<hr className="my-4 border-zinc-800" />
<p className="text-sm">
<Link to="/certifications" className="underline">
&lt; All certifications
</Link>
</p>
<hr className="my-4 border-zinc-800" />
<Footer />
</main>
);
}
+52
View File
@@ -0,0 +1,52 @@
import { Link } from "react-router-dom";
import ProjectCard from "../components/ProjectCard";
import Footer from "../components/Footer";
import { CERTIFICATIONS } from "../data/projects";
export default function Certifications() {
return (
<main className="my-12 sm:my-20">
<div className="py-2">
<p className="opacity-85 text-sm text-left">CERTIFICATIONS</p>
<h1 className="text-3xl sm:text-4xl text-balance py-2">
Professional Certifications
</h1>
<p className="py-2">
All of my personal professional certifications for different things in
the software dev /QA testing space.
</p>
</div>
<hr className="my-4 border-zinc-800" />
<section aria-labelledby="all-certifications">
<h2
id="all-certifications"
className="opacity-85 text-sm text-left pb-2"
>
ALL CERTIFICATIONS
</h2>
<div className="my-2 space-y-2">
{CERTIFICATIONS.map((project) => (
<ProjectCard
key={project.slug}
project={{ ...project, isCert: true }}
/>
))}
</div>
</section>
<hr className="my-4 border-zinc-800" />
<p className="text-sm">
<Link to="/" className="underline">
&lt; Back home
</Link>
</p>
<hr className="my-4 border-zinc-800" />
<Footer />
</main>
);
}
+18 -1
View File
@@ -7,7 +7,7 @@ import BlogRow from "../components/BlogRow";
import GameComponent from "../components/info/GameComponent";
import MusicComponent from "../components/info/MusicComponent";
import EmailLink from "../components/EmailLink";
import { PROJECTS } from "../data/projects";
import { PROJECTS, CERTIFICATIONS } from "../data/projects";
import { PROFILES } from "../utils/site";
export default function Home() {
@@ -84,6 +84,23 @@ export default function Home() {
<hr className="my-4 border-zinc-800" />
<section aria-labelledby="certifications">
<h2 id="certifications" className="opacity-85 text-sm text-left pb-2">
CERTIFICATIONS
</h2>
<div className="my-2 space-y-2">
{CERTIFICATIONS.map((project) => (
<ProjectCard
key={project.slug}
project={{ ...project, isCert: true }}
/>
))}
</div>
</section>
<hr className="my-4 border-zinc-800" />
<section aria-labelledby="my-activities">
<h2 id="my-activities" className="opacity-85 text-sm text-left pb-2">
MY ACTIVITIES
+40 -2
View File
@@ -5,13 +5,22 @@ import ProjectDetail from "./pages/ProjectDetail";
import Sports from "./pages/Sports";
import Pictures from "./pages/Pictures";
import NotFound from "./pages/NotFound";
import { PROJECTS, findProject } from "./data/projects";
import {
CERTIFICATIONS,
PROJECTS,
findProject,
findCertification,
} from "./data/projects";
import type { PageMeta } from "./utils/seo";
import {
profilePageGraph,
projectListGraph,
projectGraph,
certificationGraph,
certificationListGraph,
} from "./utils/structuredData";
import Certifications from "./pages/Certifications";
import CertificationDetail from "./pages/CertificationDetail";
type Params = Record<string, string | undefined>;
@@ -78,6 +87,35 @@ export const routes: AppRoute[] = [
};
},
},
{
path: "/certifications",
element: <Certifications />,
sitemapPaths: () => ["/certifications"],
meta: () => ({
title: "Certifications — Alex Frantz",
description:
"All of my professionally-issued certifications. From software development to software testing, I am always studying something.",
path: "/certifications",
jsonLd: certificationListGraph(PROJECTS),
}),
},
{
path: "/certifications/:slug",
element: <CertificationDetail />,
sitemapPaths: () => CERTIFICATIONS.map((p) => `/certifications/${p.slug}`),
meta: (params) => {
const certification = findCertification(params.slug);
if (!certification) return null;
return {
title: `${certification.title} — Alex Frantz`,
description: certification.description,
path: `/certifications/${certification.slug}`,
type: "article",
jsonLd: certificationGraph(certification),
};
},
},
{
path: "/sports",
element: <Sports />,
@@ -85,7 +123,7 @@ export const routes: AppRoute[] = [
meta: () => ({
title: "Bills, Sabres & Yankees Scores — Alex Frantz",
description:
"Live Buffalo Bills, Buffalo Sabres and New York Yankees scores, plus how my fantasy football matchup is going this week.",
"Live Buffalo Bills and Buffalo Sabres scores, plus how my fantasy football matchup is going this week.",
path: "/sports",
}),
},
+49
View File
@@ -119,3 +119,52 @@ export function projectGraph(project: Project): object[] {
},
];
}
export function certificationListGraph(certifications: Project[]): object[] {
return [
{
"@context": "https://schema.org",
"@type": "CollectionPage",
url: absoluteUrl("/projects"),
name: "Projects",
about: { "@id": PERSON_ID },
mainEntity: {
"@type": "ItemList",
itemListElement: certifications.map((project, index) => ({
"@type": "ListItem",
position: index + 1,
url: absoluteUrl(`/certifications/${project.slug}`),
name: project.title,
})),
},
},
];
}
export function certificationGraph(certificate: Project): object[] {
return [
{
"@context": "https://schema.org",
"@type": "SoftwareSourceCode",
name: certificate.title,
description: certificate.description,
url: absoluteUrl(`/projects/${certificate.slug}`),
codeRepository: certificate.link,
programmingLanguage: certificate.pills,
author: { "@id": PERSON_ID },
},
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Projects",
item: absoluteUrl("/certifications"),
},
{ "@type": "ListItem", position: 2, name: certificate.title },
],
},
];
}