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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

[![Netlify Status](https://api.netlify.com/api/v1/badges/4df86a71-2a3f-40f9-9bd5-b6dacd4f420c/deploy-status)](https://app.netlify.com/sites/piech-dev/deploys)

My personal page. Over time it turned into a complex project itself: it supports loading all projects' information directly from GitHub, renders GitHub's markdown, the whole page is pre-rendered and served with zero JS. It also includes dynamic <meta> tags for each project page, including individual og:image tags with sizes.
My personal page. Over time it turned into a complex project itself: it supports loading all projects' information directly from GitHub, renders GitHub's markdown, the whole page is pre-rendered and served with zero JS. It also includes dynamic <meta> tags for each project page, including individual og:image tags with sizes. All routes have appropriate JSON-LD objects with relevant information.

<img src="public/media/projects/piech.dev.webp" alt="Lighthouse results" title="Lighthouse results" width="500" />

## Dynamic, GitHub-based project list and details
## Dynamic project list pulled from GitHub-based

- Projects pull metadata and READMEs directly from GitHub at build time
- The projects/ page is managed via minimal configuration, just based on repository names. Projects metadata and READMEs are fetched directly from GitHub at build time.
- Markdown rendering transforms relative links to proper URLs and handles videos, so that you can see video previews of my projects without leaving my site.
- GitHub topics automatically become \<meta> keywords.
- GitHub repository topics automatically become \<meta> keywords.
- GitHub information, as well as images are dynamically pulled to each project's \<head> into appropriate og: tags, allowing for custom preview card of each project in social media and on messengers.

## React pre-rendering with zero JavaScript served to the user
Expand Down
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"rimraf": "^6.0.1",
"rollup-plugin-visualizer": "^6.0.5",
"sass-embedded": "^1.93.2",
"schema-dts": "^1.1.5",
"stylelint": "^16.25.0",
"stylelint-config-recommended": "^17.0.0",
"stylelint-config-recommended-scss": "^16.0.2",
Expand Down
4 changes: 1 addition & 3 deletions src/features/Projects/Projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ const Project = (): React.JSX.Element => {
<main className={styles.projects}>
<h2>Projects</h2>
<div className={'divider'} />
<p className={'smallHeadline'}>
Non-commercial projects I built in my free time.
</p>
<p className={'smallHeadline'}>Projects I built in my free time.</p>
<div className={'divider'} />
<div className={styles.projectsItemsContainer}>
{PROJECTS.map(
Expand Down
63 changes: 63 additions & 0 deletions src/routes/contact.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import React from 'react';
import type { MetaFunction } from 'react-router';
import type { BreadcrumbList, Graph, WebPage, ImageObject } from 'schema-dts';

import { PERSON, PERSON_ID, WEBSITE, WEBSITE_ID } from './index';

import {
DEFAULT_KEYWORDS,
Expand All @@ -8,10 +11,69 @@ import {
} from 'app/appConstants';
import Contact from 'features/Contact/Contact';
import { getImageSize } from 'utils/getImageSize';
import { REPOSITORY_INFO } from 'utils/githubData';

const breadcrumbList: BreadcrumbList = {
'@type': 'BreadcrumbList',
'@id': 'https://piech.dev/contact/#breadcrumb',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: 'https://piech.dev/',
},
{
'@type': 'ListItem',
position: 2,
name: 'Contact',
item: 'https://piech.dev/contact/',
},
],
};

export const meta: MetaFunction = () => {
const ogImage = 'piech.dev_contact.jpg';
const size = getImageSize(`${LOCAL_OG_IMAGES_DIRECTORY}${ogImage}`);
const imageObj: ImageObject = {
'@type': 'ImageObject',
'@id': 'https://piech.dev/contact/#image',
contentUrl: `${PRODUCTION_OG_IMAGES_DIRECTORY}${ogImage}`,
url: `${PRODUCTION_OG_IMAGES_DIRECTORY}${ogImage}`,
width: {
'@type': 'QuantitativeValue',
value: size.width,
unitText: 'px',
},
height: {
'@type': 'QuantitativeValue',
value: size.height,
unitText: 'px',
},
caption: 'Screenshot of contact links for Piotr Piech.',
};

const pageId = 'https://piech.dev/contact/#page';
const contactPage: WebPage = {
'@type': ['WebPage', 'ContactPage'] as unknown as 'WebPage',
'@id': pageId,
url: 'https://piech.dev/contact/',
name: 'Contact | piech.dev',
description: 'Contact Piotr Piech (email, LinkedIn, GitHub, Telegram).',
inLanguage: 'en',
isPartOf: { '@id': WEBSITE_ID },
mainEntity: { '@id': PERSON_ID },
breadcrumb: { '@id': 'https://piech.dev/contact/#breadcrumb' },
primaryImageOfPage: { '@id': 'https://piech.dev/contact/#image' },
image: { '@id': 'https://piech.dev/contact/#image' },
datePublished: REPOSITORY_INFO['piech.dev']?.createdDatetime,
dateModified: REPOSITORY_INFO['piech.dev']?.lastCommitDatetime,
};

const graph: Graph = {
'@context': 'https://schema.org',
'@graph': [WEBSITE, contactPage, breadcrumbList, PERSON, imageObj],
};

return [
{ title: 'Contact | piech.dev' },
Expand Down Expand Up @@ -42,6 +104,7 @@ export const meta: MetaFunction = () => {
rel: 'canonical',
href: 'https://piech.dev/contact/',
},
{ 'script:ld+json': JSON.stringify(graph) },
];
};

Expand Down
158 changes: 158 additions & 0 deletions src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,174 @@
import React from 'react';
import type { MetaFunction } from 'react-router';
import type {
EducationalOrganization,
Person,
WebSite,
Graph,
WebPage,
ContactPoint,
ImageObject,
} from 'schema-dts';

import {
DEFAULT_KEYWORDS,
LOCAL_OG_IMAGES_DIRECTORY,
PRODUCTION_OG_IMAGES_DIRECTORY,
} from 'app/appConstants';
import About from 'features/About/About';
import { PROJECTS } from 'features/Projects/projectsList';
import { TECHNOLOGIES } from 'features/Projects/technologies';
import { getImageSize } from 'utils/getImageSize';
import { REPOSITORY_INFO } from 'utils/githubData';

const alumniOf: EducationalOrganization = {
'@type': 'EducationalOrganization',
name: 'Lublin University of Technology',
};

export const PERSON_ID = 'https://piech.dev/#person';
export const WEBSITE_ID = 'https://piech.dev/#website';
export const ABOUT_ID = 'https://piech.dev/#about';
export const PIOTR_IMAGE_ID = 'https://piech.dev/#piotr-image';

const CONTACT_POINTS: ContactPoint[] = [
{
'@type': 'ContactPoint',
contactType: 'general inquiries',
email: 'piotr@piech.dev',
availableLanguage: ['en', 'pl', 'ru'],
},
{
'@type': 'ContactPoint',
contactType: 'social',
url: 'https://www.linkedin.com/in/ppiech',
availableLanguage: ['en', 'pl', 'ru'],
},
{
'@type': 'ContactPoint',
contactType: 'code repositories',
url: 'https://github.com/Tenemo',
availableLanguage: ['en', 'pl', 'ru'],
},
{
'@type': 'ContactPoint',
contactType: 'general inquiries',
url: 'https://t.me/tenemo',
availableLanguage: ['en', 'pl', 'ru'],
},
];

export const PERSON: Person = {
'@type': 'Person',
'@id': PERSON_ID,
url: 'https://piech.dev/',
jobTitle: 'Engineering Manager',
name: 'Piotr Piech',
givenName: 'Piotr',
familyName: 'Piech',
image: 'https://piech.dev/media/projects/og_images/piotr.jpg',
email: 'piotr@piech.dev',
description:
'Engineering Manager & Software Architect specializing in full-stack web development.',
alumniOf,
sameAs: [
'https://github.com/Tenemo',
'https://www.linkedin.com/in/ppiech',
'https://t.me/tenemo',
],
address: {
'@type': 'PostalAddress',
addressLocality: 'Lublin',
addressCountry: 'PL',
},
knowsLanguage: ['en', 'pl', 'ru'],
knowsAbout: Array.from(
new Set(
PROJECTS.flatMap((p) => p.technologies).map(
(t) => TECHNOLOGIES[t].fullName,
),
),
),
Comment on lines +85 to +91
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This computation runs on every meta function call, which could be inefficient during server-side rendering. Consider moving this to a constant or memoizing the result since PROJECTS and TECHNOLOGIES are static.

Copilot uses AI. Check for mistakes.
nationality: {
'@type': 'Country',
name: 'Poland',
},
workLocation: {
'@type': 'Place',
address: {
'@type': 'PostalAddress',
addressLocality: 'Lublin',
addressCountry: 'PL',
},
},
contactPoint: CONTACT_POINTS,
mainEntityOfPage: { '@id': ABOUT_ID },
};

export const WEBSITE: WebSite = {
'@type': 'WebSite',
'@id': WEBSITE_ID,
name: 'piech.dev',
alternateName: 'Piotr Piech — piech.dev',
url: 'https://piech.dev/',
inLanguage: 'en',
description: "Piotr's personal page.",
author: { '@id': PERSON_ID },
publisher: { '@id': PERSON_ID },
copyrightHolder: { '@id': PERSON_ID },
datePublished: REPOSITORY_INFO['piech.dev']?.createdDatetime,
dateModified: REPOSITORY_INFO['piech.dev']?.lastCommitDatetime,
};

export const meta: MetaFunction = () => {
const ogImage = 'piotr.jpg';
const size = getImageSize(`${LOCAL_OG_IMAGES_DIRECTORY}${ogImage}`);
const portrait: ImageObject = {
'@type': 'ImageObject',
'@id': PIOTR_IMAGE_ID,
contentUrl: `${PRODUCTION_OG_IMAGES_DIRECTORY}${ogImage}`,
url: `${PRODUCTION_OG_IMAGES_DIRECTORY}${ogImage}`,
width: {
'@type': 'QuantitativeValue',
value: size.width,
unitText: 'px',
},
height: {
'@type': 'QuantitativeValue',
value: size.height,
unitText: 'px',
},
caption: 'Portrait photo of Piotr Piech.',
};

const aboutWebPage: WebPage = {
'@type': [
'WebPage',
'AboutPage',
'ProfilePage',
] as unknown as 'WebPage',
'@id': ABOUT_ID,
url: 'https://piech.dev/',
name: 'About Piotr Piech',
description: "Piotr's personal page.",
inLanguage: 'en',
isPartOf: { '@id': WEBSITE_ID },
mainEntity: { '@id': PERSON_ID },
primaryImageOfPage: { '@id': PIOTR_IMAGE_ID },
image: { '@id': PIOTR_IMAGE_ID },
datePublished: REPOSITORY_INFO['piech.dev']?.createdDatetime,
dateModified: REPOSITORY_INFO['piech.dev']?.lastCommitDatetime,
};

const personNode: Person = {
...PERSON,
image: { '@id': PIOTR_IMAGE_ID },
};

const graph: Graph = {
'@context': 'https://schema.org',
'@graph': [WEBSITE, aboutWebPage, personNode, portrait],
};

return [
{ title: 'piech.dev' },
Expand All @@ -37,6 +194,7 @@ export const meta: MetaFunction = () => {
content: 'Portrait photo of Piotr Piech.',
},
{ tagName: 'link', rel: 'canonical', href: 'https://piech.dev/' },
{ 'script:ld+json': JSON.stringify(graph) },
];
};

Expand Down
Loading