From 88b0efc5297115657292af14bda3bc971e1ca00a Mon Sep 17 00:00:00 2001 From: Tenemo Date: Sat, 18 Oct 2025 08:11:35 +0200 Subject: [PATCH 01/11] readme update --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f8e6206..4d07687 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ My personal page. Over time it turned into a complex project itself: it supports Lighthouse results -## 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 \ keywords. +- GitHub repository topics automatically become \ keywords. - GitHub information, as well as images are dynamically pulled to each project's \ 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 From 1fa9219a777c9f13856d58ca2e9a4bab513af594 Mon Sep 17 00:00:00 2001 From: Tenemo Date: Sat, 18 Oct 2025 09:22:29 +0200 Subject: [PATCH 02/11] JSON-LD for all routes --- package-lock.json | 8 ++++ package.json | 1 + src/routes/contact.tsx | 57 ++++++++++++++++++++++++++ src/routes/index.tsx | 82 +++++++++++++++++++++++++++++++++++++ src/routes/project-item.tsx | 24 +++++++++++ src/routes/projects.tsx | 41 +++++++++++++++++++ 6 files changed, 213 insertions(+) diff --git a/package-lock.json b/package-lock.json index a081a17..566fc99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,6 +70,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", @@ -18379,6 +18380,13 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/schema-dts": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", + "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", diff --git a/package.json b/package.json index 3bf25ae..9b498cd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/routes/contact.tsx b/src/routes/contact.tsx index ffa0c13..024e4eb 100644 --- a/src/routes/contact.tsx +++ b/src/routes/contact.tsx @@ -1,5 +1,13 @@ import React from 'react'; import type { MetaFunction } from 'react-router'; +import type { + ContactPoint, + ContactPage, + WithContext, + Person, +} from 'schema-dts'; + +import { PERSON_ID } from './index'; import { DEFAULT_KEYWORDS, @@ -9,6 +17,45 @@ import { import Contact from 'features/Contact/Contact'; import { getImageSize } from 'utils/getImageSize'; +const contactPoints: ContactPoint[] = [ + { + '@type': 'ContactPoint', + contactType: 'customer support', + email: 'piotr@piech.dev', + }, + { + '@type': 'ContactPoint', + contactType: 'social', + url: 'https://www.linkedin.com/in/ppiech', + }, + { + '@type': 'ContactPoint', + contactType: 'code repository', + url: 'https://github.com/Tenemo', + }, + { + '@type': 'ContactPoint', + contactType: 'messaging', + url: 'https://t.me/tenemo', + }, +]; + +const contactPageJsonLd: WithContext = { + '@context': 'https://schema.org', + '@type': 'ContactPage', + '@id': 'https://piech.dev/contact/#page', + url: 'https://piech.dev/contact/', + name: 'Contact | piech.dev', + about: { '@id': PERSON_ID }, +}; + +const personContactJsonLd: WithContext = { + '@context': 'https://schema.org', + '@type': 'Person', + '@id': PERSON_ID, + contactPoint: contactPoints, +}; + export const meta: MetaFunction = () => { const ogImage = 'piech.dev_contact.jpg'; const size = getImageSize(`${LOCAL_OG_IMAGES_DIRECTORY}${ogImage}`); @@ -42,6 +89,16 @@ export const meta: MetaFunction = () => { rel: 'canonical', href: 'https://piech.dev/contact/', }, + { + tagName: 'script', + type: 'application/ld+json', + children: JSON.stringify(contactPageJsonLd), + }, + { + tagName: 'script', + type: 'application/ld+json', + children: JSON.stringify(personContactJsonLd), + }, ]; }; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 463ae2a..b164d8a 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,5 +1,12 @@ import React from 'react'; import type { MetaFunction } from 'react-router'; +import type { + EducationalOrganization, + Person, + WithContext, + WebSite, + AboutPage, +} from 'schema-dts'; import { DEFAULT_KEYWORDS, @@ -7,8 +14,68 @@ import { 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'; +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 PERSON: Person = { + '@type': 'Person', + '@id': PERSON_ID, + url: 'https://piech.dev/', + jobTitle: 'Engineering Manager', + name: 'Piotr Piech', + givenName: 'Piotr', + familyName: 'Piech', + alumniOf, + sameAs: [ + 'https://github.com/Tenemo', + 'https://www.linkedin.com/in/ppiech', + 'https://t.me/tenemo', + ], + knowsAbout: Array.from( + new Set( + PROJECTS.flatMap((p) => p.technologies).map( + (t) => TECHNOLOGIES[t].fullName, + ), + ), + ), +}; + +const personJsonLd: WithContext = { + '@context': 'https://schema.org', + ...PERSON, +}; + +export const websiteJsonLd: WithContext = { + '@context': 'https://schema.org', + '@type': 'WebSite', + '@id': WEBSITE_ID, + name: 'piech.dev', + url: 'https://piech.dev/', + inLanguage: 'en', + description: "Piotr's personal page.", + author: { '@id': PERSON_ID }, +}; + +const aboutPageJsonLd: WithContext = { + '@context': 'https://schema.org', + '@type': 'AboutPage', + '@id': ABOUT_ID, + image: 'https://piech.dev/media/projects/og_images/piotr.jpg', + url: 'https://piech.dev/', + name: 'About Piotr Piech', + mainEntity: { '@id': PERSON_ID }, +}; + export const meta: MetaFunction = () => { const ogImage = 'piotr.jpg'; const size = getImageSize(`${LOCAL_OG_IMAGES_DIRECTORY}${ogImage}`); @@ -37,6 +104,21 @@ export const meta: MetaFunction = () => { content: 'Portrait photo of Piotr Piech.', }, { tagName: 'link', rel: 'canonical', href: 'https://piech.dev/' }, + { + tagName: 'script', + type: 'application/ld+json', + children: JSON.stringify(personJsonLd), + }, + { + tagName: 'script', + type: 'application/ld+json', + children: JSON.stringify(websiteJsonLd), + }, + { + tagName: 'script', + type: 'application/ld+json', + children: JSON.stringify(aboutPageJsonLd), + }, ]; }; diff --git a/src/routes/project-item.tsx b/src/routes/project-item.tsx index 6a85c9d..049bde8 100644 --- a/src/routes/project-item.tsx +++ b/src/routes/project-item.tsx @@ -1,5 +1,6 @@ import React from 'react'; import type { MetaFunction } from 'react-router'; +import type { SoftwareSourceCode, WithContext } from 'schema-dts'; import { DEFAULT_KEYWORDS, @@ -11,6 +12,22 @@ import { PROJECTS } from 'features/Projects/projectsList'; import { getImageSize } from 'utils/getImageSize'; import { REPOSITORY_INFO } from 'utils/githubData'; +const buildProjectJsonLd = ( + repo: string, + desc: string, + info?: { topics?: string[]; createdDatetime?: string }, +): WithContext => ({ + '@context': 'https://schema.org', + '@type': 'SoftwareSourceCode', + name: repo, + description: desc, + url: `https://piech.dev/projects/${repo}`, + codeRepository: `https://github.com/Tenemo/${repo}`, + programmingLanguage: 'TypeScript', + keywords: info?.topics, + dateCreated: info?.createdDatetime, +}); + export const meta: MetaFunction = (args) => { const repo = args.params.repo ?? ''; const info = REPOSITORY_INFO[repo]; @@ -33,6 +50,8 @@ export const meta: MetaFunction = (args) => { const ogImageAlt = projectEntry.ogImageAlt; const size = getImageSize(`${LOCAL_OG_IMAGES_DIRECTORY}${ogImage}`); + const jsonLd = buildProjectJsonLd(repo, desc, info); + return [ { title }, { name: 'description', content: desc }, @@ -53,6 +72,11 @@ export const meta: MetaFunction = (args) => { rel: 'canonical', href: `https://piech.dev/projects/${repo}`, }, + { + tagName: 'script', + type: 'application/ld+json', + children: JSON.stringify(jsonLd), + }, ]; }; diff --git a/src/routes/projects.tsx b/src/routes/projects.tsx index 7328716..48e60e1 100644 --- a/src/routes/projects.tsx +++ b/src/routes/projects.tsx @@ -1,5 +1,12 @@ import React from 'react'; import type { MetaFunction } from 'react-router'; +import type { + CollectionPage, + ListItem, + SoftwareSourceCode, + WithContext, + ItemList, +} from 'schema-dts'; import { DEFAULT_KEYWORDS, @@ -7,8 +14,37 @@ import { PRODUCTION_OG_IMAGES_DIRECTORY, } from 'app/appConstants'; import Projects from 'features/Projects/Projects'; +import { PROJECTS } from 'features/Projects/projectsList'; import { getImageSize } from 'utils/getImageSize'; +const projectsItemList: ItemList = { + '@type': 'ItemList', + itemListElement: PROJECTS.map(({ repoName, project }, i) => { + const name = repoName ?? project; + const code: SoftwareSourceCode = { + '@type': 'SoftwareSourceCode', + '@id': `https://piech.dev/projects/${name}#code`, + name, + url: `https://piech.dev/projects/${name}`, + codeRepository: `https://github.com/Tenemo/${name}`, + programmingLanguage: 'TypeScript', + }; + return { + '@type': 'ListItem', + position: i + 1, + item: code, + }; + }), +}; + +const collectionJsonLd: WithContext = { + '@context': 'https://schema.org', + '@type': 'CollectionPage', + name: 'Projects | piech.dev', + url: 'https://piech.dev/projects/', + mainEntity: projectsItemList, +}; + export const meta: MetaFunction = () => { const ogImage = 'piech.dev_projects.jpg'; const size = getImageSize(`${LOCAL_OG_IMAGES_DIRECTORY}${ogImage}`); @@ -44,6 +80,11 @@ export const meta: MetaFunction = () => { rel: 'canonical', href: 'https://piech.dev/projects/', }, + { + tagName: 'script', + type: 'application/ld+json', + children: JSON.stringify(collectionJsonLd), + }, ]; }; From 4bd37fe7ac085ff7a80c39ca3c8c593489c1e81b Mon Sep 17 00:00:00 2001 From: Tenemo Date: Sat, 18 Oct 2025 09:38:00 +0200 Subject: [PATCH 03/11] JSON-LD tweaks --- src/features/Projects/Projects.tsx | 4 +- src/routes/contact.tsx | 31 +++++++- src/routes/index.tsx | 10 +++ src/routes/project-item.tsx | 68 ++++++++++++----- src/routes/projects.tsx | 31 +++++++- src/types/github-data.ts | 4 + src/utils/fetchGithubData.ts | 118 +++++++++++++++++++++++++++++ 7 files changed, 241 insertions(+), 25 deletions(-) diff --git a/src/features/Projects/Projects.tsx b/src/features/Projects/Projects.tsx index 68ae26e..efb9d7a 100644 --- a/src/features/Projects/Projects.tsx +++ b/src/features/Projects/Projects.tsx @@ -9,9 +9,7 @@ const Project = (): React.JSX.Element => {

Projects

-

- Non-commercial projects I built in my free time. -

+

Projects I built in my free time.

{PROJECTS.map( diff --git a/src/routes/contact.tsx b/src/routes/contact.tsx index 024e4eb..ba7825f 100644 --- a/src/routes/contact.tsx +++ b/src/routes/contact.tsx @@ -5,6 +5,7 @@ import type { ContactPage, WithContext, Person, + BreadcrumbList, } from 'schema-dts'; import { PERSON_ID } from './index'; @@ -20,8 +21,9 @@ import { getImageSize } from 'utils/getImageSize'; const contactPoints: ContactPoint[] = [ { '@type': 'ContactPoint', - contactType: 'customer support', + contactType: 'recruitment, IT services, business inquiries', email: 'piotr@piech.dev', + availableLanguage: ['en', 'pl', 'ru'], }, { '@type': 'ContactPoint', @@ -40,13 +42,35 @@ const contactPoints: ContactPoint[] = [ }, ]; +const breadcrumbJsonLd: WithContext = { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { + '@type': 'ListItem', + position: 1, + name: 'Home', + item: 'https://piech.dev/', + }, + { + '@type': 'ListItem', + position: 2, + name: 'Contact', + item: 'https://piech.dev/contact/', + }, + ], +}; + const contactPageJsonLd: WithContext = { '@context': 'https://schema.org', '@type': 'ContactPage', '@id': 'https://piech.dev/contact/#page', url: 'https://piech.dev/contact/', name: 'Contact | piech.dev', + description: 'Contact Piotr Piech (email, LinkedIn, GitHub, Telegram).', + inLanguage: 'en', about: { '@id': PERSON_ID }, + mainEntity: { '@id': PERSON_ID }, }; const personContactJsonLd: WithContext = { @@ -99,6 +123,11 @@ export const meta: MetaFunction = () => { type: 'application/ld+json', children: JSON.stringify(personContactJsonLd), }, + { + tagName: 'script', + type: 'application/ld+json', + children: JSON.stringify(breadcrumbJsonLd), + }, ]; }; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index b164d8a..fc6a13f 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -35,12 +35,19 @@ export const PERSON: Person = { name: 'Piotr Piech', givenName: 'Piotr', familyName: 'Piech', + image: 'https://piech.dev/media/projects/og_images/piotr.jpg', + email: 'piotr@piech.dev', alumniOf, sameAs: [ 'https://github.com/Tenemo', 'https://www.linkedin.com/in/ppiech', 'https://t.me/tenemo', ], + address: { + '@type': 'PostalAddress', + addressLocality: 'Lublin', + addressCountry: 'PL', + }, knowsAbout: Array.from( new Set( PROJECTS.flatMap((p) => p.technologies).map( @@ -73,7 +80,10 @@ const aboutPageJsonLd: WithContext = { image: 'https://piech.dev/media/projects/og_images/piotr.jpg', url: 'https://piech.dev/', name: 'About Piotr Piech', + description: "Piotr's personal page.", + inLanguage: 'en', mainEntity: { '@id': PERSON_ID }, + publisher: { '@id': PERSON_ID }, }; export const meta: MetaFunction = () => { diff --git a/src/routes/project-item.tsx b/src/routes/project-item.tsx index 049bde8..f4dbbef 100644 --- a/src/routes/project-item.tsx +++ b/src/routes/project-item.tsx @@ -1,6 +1,12 @@ import React from 'react'; import type { MetaFunction } from 'react-router'; -import type { SoftwareSourceCode, WithContext } from 'schema-dts'; +import type { + BreadcrumbList, + SoftwareSourceCode, + WithContext, +} from 'schema-dts'; + +import { PERSON_ID } from './index'; import { DEFAULT_KEYWORDS, @@ -12,22 +18,6 @@ import { PROJECTS } from 'features/Projects/projectsList'; import { getImageSize } from 'utils/getImageSize'; import { REPOSITORY_INFO } from 'utils/githubData'; -const buildProjectJsonLd = ( - repo: string, - desc: string, - info?: { topics?: string[]; createdDatetime?: string }, -): WithContext => ({ - '@context': 'https://schema.org', - '@type': 'SoftwareSourceCode', - name: repo, - description: desc, - url: `https://piech.dev/projects/${repo}`, - codeRepository: `https://github.com/Tenemo/${repo}`, - programmingLanguage: 'TypeScript', - keywords: info?.topics, - dateCreated: info?.createdDatetime, -}); - export const meta: MetaFunction = (args) => { const repo = args.params.repo ?? ''; const info = REPOSITORY_INFO[repo]; @@ -50,7 +40,42 @@ export const meta: MetaFunction = (args) => { const ogImageAlt = projectEntry.ogImageAlt; const size = getImageSize(`${LOCAL_OG_IMAGES_DIRECTORY}${ogImage}`); - const jsonLd = buildProjectJsonLd(repo, desc, info); + const projectJsonLd: WithContext = { + '@context': 'https://schema.org', + '@type': 'SoftwareSourceCode', + name: repo, + description: desc, + url: `https://piech.dev/projects/${repo}`, + codeRepository: `https://github.com/Tenemo/${repo}`, + programmingLanguage: 'TypeScript', + keywords: info?.topics, + dateCreated: info?.createdDatetime, + author: { '@id': PERSON_ID }, + }; + + const breadcrumbJsonLd: WithContext = { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { + '@type': 'ListItem', + position: 1, + name: 'Home', + item: 'https://piech.dev/', + }, + { + '@type': 'ListItem', + position: 2, + name: 'Contact', + item: 'https://piech.dev/projects/', + }, + { + '@type': 'ListItem', + position: 3, + name: repo, + }, + ], + }; return [ { title }, @@ -75,7 +100,12 @@ export const meta: MetaFunction = (args) => { { tagName: 'script', type: 'application/ld+json', - children: JSON.stringify(jsonLd), + children: JSON.stringify(projectJsonLd), + }, + { + tagName: 'script', + type: 'application/ld+json', + children: JSON.stringify(breadcrumbJsonLd), }, ]; }; diff --git a/src/routes/projects.tsx b/src/routes/projects.tsx index 48e60e1..010d8ba 100644 --- a/src/routes/projects.tsx +++ b/src/routes/projects.tsx @@ -6,6 +6,7 @@ import type { SoftwareSourceCode, WithContext, ItemList, + BreadcrumbList, } from 'schema-dts'; import { @@ -42,9 +43,30 @@ const collectionJsonLd: WithContext = { '@type': 'CollectionPage', name: 'Projects | piech.dev', url: 'https://piech.dev/projects/', + description: 'Projects built by Piotr Piech', + inLanguage: 'en', mainEntity: projectsItemList, }; +const breadcrumbJsonLd: WithContext = { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { + '@type': 'ListItem', + position: 1, + name: 'Home', + item: 'https://piech.dev/', + }, + { + '@type': 'ListItem', + position: 2, + name: 'Contact', + item: 'https://piech.dev/projects/', + }, + ], +}; + export const meta: MetaFunction = () => { const ogImage = 'piech.dev_projects.jpg'; const size = getImageSize(`${LOCAL_OG_IMAGES_DIRECTORY}${ogImage}`); @@ -54,14 +76,14 @@ export const meta: MetaFunction = () => { { name: 'description', content: - 'Non-commercial projects I built in my free time: small tools, libraries, and experiments in React, TypeScript, cryptography, and more.', + 'Projects I built in my free time: small tools, libraries, and experiments in React, TypeScript, cryptography, and more.', }, { name: 'keywords', content: DEFAULT_KEYWORDS }, { property: 'og:title', content: 'Projects | piech.dev' }, { property: 'og:description', content: - 'Non-commercial projects I built in my free time: small tools, libraries, and experiments in React, TypeScript, cryptography, and more.', + 'Projects I built in my free time: small tools, libraries, and experiments in React, TypeScript, cryptography, and more.', }, { property: 'og:type', content: 'website' }, { @@ -85,6 +107,11 @@ export const meta: MetaFunction = () => { type: 'application/ld+json', children: JSON.stringify(collectionJsonLd), }, + { + tagName: 'script', + type: 'application/ld+json', + children: JSON.stringify(breadcrumbJsonLd), + }, ]; }; diff --git a/src/types/github-data.ts b/src/types/github-data.ts index 9ccb736..9675087 100644 --- a/src/types/github-data.ts +++ b/src/types/github-data.ts @@ -3,6 +3,10 @@ export type RepositoryInfo = { description: string; topics?: string[]; createdDatetime: string; + // ISO string of the last commit datetime on the default or known branch + lastCommitDatetime?: string; + // SPDX license string from package.json (if present) + license?: string; }; export type GithubData = { diff --git a/src/utils/fetchGithubData.ts b/src/utils/fetchGithubData.ts index 7cad95a..10796f4 100644 --- a/src/utils/fetchGithubData.ts +++ b/src/utils/fetchGithubData.ts @@ -30,6 +30,8 @@ type RepoInfo = { description: string; topics?: string[]; createdDatetime: string; + lastCommitDatetime?: string; + license?: string; }; export type GithubData = { METADATA: { fetchedDatetime: string }; @@ -106,6 +108,75 @@ async function getReadme(owner: string, repo: string): Promise { return '# README not found\n'; } +async function getPackageJsonLicenseFromMaster( + owner: string, + repo: string, +): Promise { + // Spec: license from package.json on master HEAD commit, if present. + // We intentionally only check the 'master' branch as requested, + // and do not fallback to 'main' here. + try { + const raw = await fetchText( + `https://raw.githubusercontent.com/${owner}/${repo}/master/package.json`, + ); + try { + const pkg = JSON.parse(raw) as unknown; + if ( + pkg && + typeof pkg === 'object' && + 'license' in (pkg as Record) + ) { + const lic = (pkg as Record).license; + if (typeof lic === 'string') return lic; + if ( + lic && + typeof lic === 'object' && + 'type' in (lic as Record) && + typeof (lic as Record).type === 'string' + ) + return (lic as Record).type as string; + } + // Support legacy `licenses` array + if (pkg && typeof pkg === 'object') { + const anyPkg = pkg as Record; + const licensesRaw = anyPkg.licenses; + if (Array.isArray(licensesRaw) && licensesRaw.length > 0) { + const first = licensesRaw[0] as { type?: unknown }; + if (typeof first.type === 'string') return first.type; + } + } + } catch { + // ignore JSON parsing errors and fall through + } + } catch { + // master/package.json not found or inaccessible + } + return undefined; +} + +async function getLastCommitDatetime( + owner: string, + repo: string, + token: string | undefined, + branch: string, +): Promise { + // Use the commits API to get the head commit for the branch. + // If this fails (e.g. branch doesn't exist), callers will try fallbacks. + try { + const res = await fetchJson<{ + sha: string; + commit?: { author?: { date?: string } }; + }>( + `https://api.github.com/repos/${owner}/${repo}/commits/${branch}`, + token, + ); + const date = res.commit?.author?.date; + return typeof date === 'string' ? date : undefined; + } catch { + return undefined; + } +} + export async function fetchGithubData(options?: { refetch?: boolean; }): Promise { @@ -165,6 +236,7 @@ export async function fetchGithubData(options?: { name?: string; description?: string; created_at?: string; + default_branch?: string; }>(`https://api.github.com/repos/${OWNER}/${repo}`, token), fetchJson<{ names?: string[] }>( `https://api.github.com/repos/${OWNER}/${repo}/topics`, @@ -175,21 +247,67 @@ export async function fetchGithubData(options?: { if (infoRes.status === 'fulfilled') { const repoData = infoRes.value; + // Attempt to populate lastCommitDatetime using the default branch when available. + let lastCommitDatetime: string | undefined; + if (repoData.default_branch) { + lastCommitDatetime = await getLastCommitDatetime( + OWNER, + repo, + token, + repoData.default_branch, + ); + } else { + // Fallback to trying known branch names. + for (const b of BRANCHES) { + lastCommitDatetime = await getLastCommitDatetime( + OWNER, + repo, + token, + b, + ); + if (lastCommitDatetime) break; + } + } + + // License from master/package.json only (per spec) + const license = await getPackageJsonLicenseFromMaster(OWNER, repo); + infoObject[repo] = { name: repoData.name ?? repo, description: repoData.description ?? 'No description available', createdDatetime: repoData.created_at ?? EPOCH_ISO, + lastCommitDatetime, + license, topics: topicsRes.status === 'fulfilled' ? (topicsRes.value.names ?? []) : undefined, }; } else { + // Even if repo info failed, still attempt best-effort for lastCommitDatetime and license + let lastCommitDatetime: string | undefined; + for (const b of BRANCHES) { + // Try known branches; ignore failures + const maybe = await getLastCommitDatetime( + OWNER, + repo, + token, + b, + ); + if (maybe) { + lastCommitDatetime = maybe; + break; + } + } + const license = await getPackageJsonLicenseFromMaster(OWNER, repo); + infoObject[repo] = { name: repo, description: 'No description available', // Fallback to epoch to make failures obvious createdDatetime: EPOCH_ISO, + lastCommitDatetime, + license, topics: topicsRes.status === 'fulfilled' ? (topicsRes.value.names ?? []) From 25c17c37a9c8241c2dbf352f91c7cbd4f27e8817 Mon Sep 17 00:00:00 2001 From: Tenemo Date: Sat, 18 Oct 2025 10:03:45 +0200 Subject: [PATCH 04/11] JSON-LD scripts injected properly; @graph instead of multiple