From 73e72c35a8ec482f8498bf68feb8ba341886ef44 Mon Sep 17 00:00:00 2001 From: Tenemo Date: Sat, 18 Oct 2025 12:06:49 +0200 Subject: [PATCH 1/8] GitHub data fetching refactor --- .vscode/tasks.json | 48 ++++++------- .../Projects/ProjectCard/ProjectCard.tsx | 4 +- .../Projects/ProjectItem/ProjectItem.tsx | 4 +- .../ProjectMarkdown/ProjectMarkdown.tsx | 4 +- src/mocks/mockGithubData.ts | 47 ++++++++++--- src/routes/contact.tsx | 6 +- src/routes/index.tsx | 10 +-- src/routes/project-item.tsx | 4 +- src/routes/projects.tsx | 6 +- src/types/github-data.ts | 8 +-- src/utils/fetchGithubData.ts | 69 ++++++++++--------- src/utils/githubData.ts | 11 +-- src/utils/testsSetup.ts | 9 +-- 13 files changed, 129 insertions(+), 101 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8c5efde..580e936 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,28 +1,22 @@ { - "version": "2.0.0", - "tasks": [ - { - "label": "Run tests", - "type": "shell", - "command": "npm test --silent", - "problemMatcher": [ - "$tsc", - "$eslint-stylish" - ] - }, - { - "label": "Run tests", - "type": "shell", - "command": "npm test --silent", - "problemMatcher": [ - "$tsc", - "$eslint-stylish" - ] - }, - { - "label": "Typecheck", - "type": "shell", - "command": "npm run tsc --silent" - } - ] -} \ No newline at end of file + "version": "2.0.0", + "tasks": [ + { + "label": "Run tests", + "type": "shell", + "command": "npm test --silent", + "problemMatcher": ["$tsc", "$eslint-stylish"] + }, + { + "label": "Run tests", + "type": "shell", + "command": "npm test --silent", + "problemMatcher": ["$tsc", "$eslint-stylish"] + }, + { + "label": "Typecheck", + "type": "shell", + "command": "npm run tsc --silent" + } + ] +} diff --git a/src/features/Projects/ProjectCard/ProjectCard.tsx b/src/features/Projects/ProjectCard/ProjectCard.tsx index a95d445..b3af4a4 100644 --- a/src/features/Projects/ProjectCard/ProjectCard.tsx +++ b/src/features/Projects/ProjectCard/ProjectCard.tsx @@ -8,7 +8,7 @@ import { TECHNOLOGIES } from '../technologies'; import styles from './projectCard.module.scss'; import ProjectTechnologies from './ProjectTechnologies/ProjectTechnologies'; -import { REPOSITORY_INFO } from 'utils/githubData'; +import { REPOSITORIES } from 'utils/githubData'; type ProjectCardProps = { projectPreview: string; @@ -26,7 +26,7 @@ const ProjectCard = ({ repoName, }: ProjectCardProps): React.JSX.Element => { const githubRepository = repoName ?? project; - const repositoryInfo = REPOSITORY_INFO[githubRepository]; + const repositoryInfo = REPOSITORIES[githubRepository]; const createdIso = repositoryInfo?.createdDatetime; const createdLabel = createdIso ? format(new Date(createdIso), 'MMMM yyyy') diff --git a/src/features/Projects/ProjectItem/ProjectItem.tsx b/src/features/Projects/ProjectItem/ProjectItem.tsx index 3ca19aa..51fca88 100644 --- a/src/features/Projects/ProjectItem/ProjectItem.tsx +++ b/src/features/Projects/ProjectItem/ProjectItem.tsx @@ -6,7 +6,7 @@ import { Link, useParams } from 'react-router'; import styles from './projectItem.module.scss'; import ProjectMarkdown from './ProjectMarkdown/ProjectMarkdown'; -import { README_CONTENT } from 'utils/githubData'; +import { REPOSITORIES } from 'utils/githubData'; const ProjectItemDetails: React.FC = (): React.JSX.Element => { const { repo } = useParams<{ repo: string }>(); @@ -19,7 +19,7 @@ const ProjectItemDetails: React.FC = (): React.JSX.Element => { ); } - const markdown = README_CONTENT[repo] ?? ''; + const markdown = REPOSITORIES[repo]?.readme_content ?? ''; return (
diff --git a/src/features/Projects/ProjectItem/ProjectMarkdown/ProjectMarkdown.tsx b/src/features/Projects/ProjectItem/ProjectMarkdown/ProjectMarkdown.tsx index 007f930..6ad2583 100644 --- a/src/features/Projects/ProjectItem/ProjectMarkdown/ProjectMarkdown.tsx +++ b/src/features/Projects/ProjectItem/ProjectMarkdown/ProjectMarkdown.tsx @@ -10,7 +10,7 @@ import remarkGfm from 'remark-gfm'; import styles from './projectMarkdown.module.scss'; -import { REPOSITORY_INFO } from 'utils/githubData'; +import { REPOSITORIES } from 'utils/githubData'; const OWNER = 'tenemo'; const BRANCH = 'master'; @@ -26,7 +26,7 @@ const ProjectMarkdown = ({ markdown, repo, }: ProjectMarkdownProps): React.JSX.Element => { - const createdIso = REPOSITORY_INFO[repo]?.createdDatetime; + const createdIso = REPOSITORIES[repo]?.createdDatetime; const createdLabel = createdIso ? format(new Date(createdIso), 'MMMM yyyy') : undefined; diff --git a/src/mocks/mockGithubData.ts b/src/mocks/mockGithubData.ts index 4082909..016258a 100644 --- a/src/mocks/mockGithubData.ts +++ b/src/mocks/mockGithubData.ts @@ -1,61 +1,90 @@ import type { GithubData } from 'types/github-data'; +const baseInfo = { + lastCommitDatetime: '2025-01-02T00:00:00.000Z', + readme_content: '# Placeholder', +}; + const data: GithubData = { - METADATA: { fetchedDatetime: '2025-01-01T00:00:00.000Z' }, - REPOSITORY_INFO: { + metadata: { fetchedDatetime: '2025-01-01T00:00:00.000Z' }, + repositories: { 'test-project': { name: 'test-project', description: 'Test description', topics: ['react', 'typescript'], createdDatetime: '2020-05-01T12:00:00.000Z', + ...baseInfo, }, 'img-test': { name: 'img-test', description: 'Image test', topics: ['images'], createdDatetime: '2023-09-12T08:00:00.000Z', + ...baseInfo, }, 'video-test': { name: 'video-test', description: 'Video test', topics: ['video'], createdDatetime: '2022-01-20T15:30:00.000Z', + ...baseInfo, }, 'right-test': { name: 'right-test', description: 'Right image test', topics: ['ui'], createdDatetime: '2018-07-15T00:00:00.000Z', + ...baseInfo, }, 'custom-repo': { name: 'custom-repo', description: 'Custom repo test', createdDatetime: '2022-03-05T08:15:00.000Z', + ...baseInfo, }, 'cached-repo': { name: 'cached-repo', description: 'Cached repo', topics: [], createdDatetime: '2017-01-01T00:00:00.000Z', + ...baseInfo, }, 'link-test': { name: 'link-test', description: 'Link test', createdDatetime: '2018-07-07T07:07:07.000Z', + ...baseInfo, }, 'epoch-test': { name: 'epoch-test', description: 'Epoch fallback', createdDatetime: '1970-01-01T00:00:00.000Z', + ...baseInfo, + }, + 'test-repo': { + name: 'test-repo', + description: 'Test Readme Content', + createdDatetime: '2020-01-01T00:00:00.000Z', + ...baseInfo, + readme_content: '# Test Readme Content', + }, + 'cached-project': { + name: 'cached-project', + description: 'Cached Readme Content', + createdDatetime: '2020-01-01T00:00:00.000Z', + ...baseInfo, + readme_content: '# Cached Readme Content', + }, + 'new-content-project': { + name: 'new-content-project', + description: 'New Readme Content', + createdDatetime: '2020-01-01T00:00:00.000Z', + ...baseInfo, + readme_content: '# New Readme Content', }, - }, - README_CONTENT: { - 'test-repo': '# Test Readme Content', - 'cached-project': '# Cached Readme Content', - 'new-content-project': '# New Readme Content', }, }; -export const REPOSITORY_INFO = data.REPOSITORY_INFO; -export const README_CONTENT = data.README_CONTENT; +export const REPOSITORIES = data.repositories; +export const METADATA = data.metadata; export default data; diff --git a/src/routes/contact.tsx b/src/routes/contact.tsx index 6de5f86..39e8029 100644 --- a/src/routes/contact.tsx +++ b/src/routes/contact.tsx @@ -11,7 +11,7 @@ import { } from 'app/appConstants'; import Contact from 'features/Contact/Contact'; import { getImageSize } from 'utils/getImageSize'; -import { REPOSITORY_INFO } from 'utils/githubData'; +import { REPOSITORIES } from 'utils/githubData'; const breadcrumbList: BreadcrumbList = { '@type': 'BreadcrumbList', @@ -66,8 +66,8 @@ export const meta: MetaFunction = () => { 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, + datePublished: REPOSITORIES['piech.dev']?.createdDatetime, + dateModified: REPOSITORIES['piech.dev']?.lastCommitDatetime, }; const graph: Graph = { diff --git a/src/routes/index.tsx b/src/routes/index.tsx index d8c3454..3582823 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -19,7 +19,7 @@ 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'; +import { REPOSITORIES } from 'utils/githubData'; const alumniOf: EducationalOrganization = { '@type': 'EducationalOrganization', @@ -116,8 +116,8 @@ export const WEBSITE: WebSite = { author: { '@id': PERSON_ID }, publisher: { '@id': PERSON_ID }, copyrightHolder: { '@id': PERSON_ID }, - datePublished: REPOSITORY_INFO['piech.dev']?.createdDatetime, - dateModified: REPOSITORY_INFO['piech.dev']?.lastCommitDatetime, + datePublished: REPOSITORIES['piech.dev']?.createdDatetime, + dateModified: REPOSITORIES['piech.dev']?.lastCommitDatetime, }; export const meta: MetaFunction = () => { @@ -156,8 +156,8 @@ export const meta: MetaFunction = () => { 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, + datePublished: REPOSITORIES['piech.dev']?.createdDatetime, + dateModified: REPOSITORIES['piech.dev']?.lastCommitDatetime, }; const personNode: Person = { diff --git a/src/routes/project-item.tsx b/src/routes/project-item.tsx index a06b89e..981d86f 100644 --- a/src/routes/project-item.tsx +++ b/src/routes/project-item.tsx @@ -18,11 +18,11 @@ import { import ProjectItem from 'features/Projects/ProjectItem/ProjectItem'; import { PROJECTS } from 'features/Projects/projectsList'; import { getImageSize } from 'utils/getImageSize'; -import { REPOSITORY_INFO } from 'utils/githubData'; +import { REPOSITORIES } from 'utils/githubData'; export const meta: MetaFunction = (args) => { const repo = args.params.repo ?? ''; - const info = REPOSITORY_INFO[repo]; + const info = REPOSITORIES[repo]; const title = `${repo} | piech.dev`; const desc = info?.description ?? diff --git a/src/routes/projects.tsx b/src/routes/projects.tsx index 81e95e2..fb302b5 100644 --- a/src/routes/projects.tsx +++ b/src/routes/projects.tsx @@ -20,7 +20,7 @@ import { import Projects from 'features/Projects/Projects'; import { PROJECTS } from 'features/Projects/projectsList'; import { getImageSize } from 'utils/getImageSize'; -import { REPOSITORY_INFO } from 'utils/githubData'; +import { REPOSITORIES } from 'utils/githubData'; const projectsItemList: ItemList = { '@type': 'ItemList', @@ -99,8 +99,8 @@ export const meta: MetaFunction = () => { mainEntity: projectsItemList, primaryImageOfPage: { '@id': 'https://piech.dev/projects/#main-image' }, image: { '@id': 'https://piech.dev/projects/#main-image' }, - datePublished: REPOSITORY_INFO['piech.dev']?.createdDatetime, - dateModified: REPOSITORY_INFO['piech.dev']?.lastCommitDatetime, + datePublished: REPOSITORIES['piech.dev']?.createdDatetime, + dateModified: REPOSITORIES['piech.dev']?.lastCommitDatetime, }; const graph: Graph = { diff --git a/src/types/github-data.ts b/src/types/github-data.ts index 60a6360..b33ddda 100644 --- a/src/types/github-data.ts +++ b/src/types/github-data.ts @@ -3,12 +3,12 @@ export type RepositoryInfo = { description: string; topics?: string[]; createdDatetime: string; - lastCommitDatetime?: string; + lastCommitDatetime: string; // must always be present license?: string; + readme_content: string; }; export type GithubData = { - METADATA?: { fetchedDatetime: string }; - REPOSITORY_INFO: Partial>; - README_CONTENT: Partial>; + metadata?: { fetchedDatetime: string }; + repositories: Partial>; }; diff --git a/src/utils/fetchGithubData.ts b/src/utils/fetchGithubData.ts index 09d2713..630bad5 100644 --- a/src/utils/fetchGithubData.ts +++ b/src/utils/fetchGithubData.ts @@ -3,9 +3,18 @@ - Programmatic usage: await fetchGithubData({ refetch?: boolean }) - Writes temp/githubData.json with shape: { - METADATA: { fetchedDatetime: string }, - REPOSITORY_INFO: { [repo]: { name, description, topics?: string[], createdDatetime: string } }, - README_CONTENT: { [repo]: markdown } + metadata: { fetchedDatetime: string }, + repositories: { + [repo]: { + name: string, + description: string, + topics?: string[], + createdDatetime: string, + lastCommitDatetime: string, + license?: string, + readme_content: string, + } + } } */ @@ -30,13 +39,13 @@ type RepoInfo = { description: string; topics?: string[]; createdDatetime: string; - lastCommitDatetime?: string; + lastCommitDatetime: string; // required license?: string; + readme_content: string; }; export type GithubData = { - METADATA: { fetchedDatetime: string }; - REPOSITORY_INFO: Record; - README_CONTENT: Record; + metadata: { fetchedDatetime: string }; + repositories: Record; }; function parseReposFromProjectsFile(text: string): string[] { @@ -196,7 +205,7 @@ export async function fetchGithubData(options?: { const current = JSON.parse(raw) as Partial; // Determine if the file is older than one day - const metaStr = current.METADATA?.fetchedDatetime; + const metaStr = current.metadata?.fetchedDatetime; const metaDate = metaStr ? new Date(metaStr) : undefined; const fileMtimeMs = fssync.statSync(OUT_PATH).mtime.getTime(); const effectiveTimeMs = @@ -206,11 +215,8 @@ export async function fetchGithubData(options?: { const ONE_DAY_MS = 24 * 60 * 60 * 1000; const olderThanOneDay = Date.now() - effectiveTimeMs > ONE_DAY_MS; - const infoKeys = Object.keys(current.REPOSITORY_INFO ?? {}); - const readmeKeys = Object.keys(current.README_CONTENT ?? {}); - const complete = repos.every( - (r) => infoKeys.includes(r) && readmeKeys.includes(r), - ); + const infoKeys = Object.keys(current.repositories ?? {}); + const complete = repos.every((r) => infoKeys.includes(r)); if (!refetch && complete && !olderThanOneDay) { console.log( '[githubData] Up-to-date and fresh file found, skipping.', @@ -228,7 +234,6 @@ export async function fetchGithubData(options?: { } const infoObject: Record = {}; - const readmeObject: Record = {}; const MAX_CONCURRENCY = Number(process.env.GH_CONCURRENCY ?? 8); const EPOCH_ISO = '1970-01-01T00:00:00.000Z'; @@ -249,6 +254,17 @@ export async function fetchGithubData(options?: { getReadme(OWNER, repo), ]); + let readmeContent = '# README unavailable\n'; + if (readmeRes.status === 'fulfilled') { + readmeContent = readmeRes.value; + } else { + console.warn( + '[githubData] README fetch failed for', + repo, + stringifyReason(readmeRes.reason), + ); + } + if (infoRes.status === 'fulfilled') { const repoData = infoRes.value; // Attempt to populate lastCommitDatetime using the default branch when available. @@ -280,8 +296,10 @@ export async function fetchGithubData(options?: { name: repoData.name ?? repo, description: repoData.description ?? 'No description available', createdDatetime: repoData.created_at ?? EPOCH_ISO, - lastCommitDatetime, + lastCommitDatetime: + lastCommitDatetime ?? repoData.created_at ?? EPOCH_ISO, license, + readme_content: readmeContent, topics: topicsRes.status === 'fulfilled' ? (topicsRes.value.names ?? []) @@ -310,8 +328,9 @@ export async function fetchGithubData(options?: { description: 'No description available', // Fallback to epoch to make failures obvious createdDatetime: EPOCH_ISO, - lastCommitDatetime, + lastCommitDatetime: lastCommitDatetime ?? EPOCH_ISO, license, + readme_content: readmeContent, topics: topicsRes.status === 'fulfilled' ? (topicsRes.value.names ?? []) @@ -332,17 +351,6 @@ export async function fetchGithubData(options?: { stringifyReason(topicsRes.reason), ); } - - if (readmeRes.status === 'fulfilled') { - readmeObject[repo] = readmeRes.value; - } else { - readmeObject[repo] = '# README unavailable\n'; - console.warn( - '[githubData] README fetch failed for', - repo, - stringifyReason(readmeRes.reason), - ); - } }; if (repos.length <= MAX_CONCURRENCY) { @@ -362,12 +370,11 @@ export async function fetchGithubData(options?: { await fs.mkdir(OUT_DIR, { recursive: true }); const payload: GithubData = { - METADATA: { fetchedDatetime: new Date().toISOString() }, - REPOSITORY_INFO: infoObject, - README_CONTENT: readmeObject, + metadata: { fetchedDatetime: new Date().toISOString() }, + repositories: infoObject, }; await fs.writeFile(OUT_PATH, JSON.stringify(payload, null, 2), 'utf8'); - console.log('[githubData] Wrote', OUT_PATH); + console.log('[githubData] Wrote new format to', OUT_PATH); } // CLI diff --git a/src/utils/githubData.ts b/src/utils/githubData.ts index 11f9d50..950372f 100644 --- a/src/utils/githubData.ts +++ b/src/utils/githubData.ts @@ -8,10 +8,11 @@ /* eslint-disable */ import githubData from '../../temp/githubData.json'; -import type { RepositoryInfo } from 'types/github-data'; +import type { GithubData, RepositoryInfo } from 'types/github-data'; /* eslint-enable */ -export const REPOSITORY_INFO: Partial> = - githubData.REPOSITORY_INFO; -export const README_CONTENT: Partial> = - githubData.README_CONTENT; +const typed: GithubData = githubData as unknown as GithubData; + +export const REPOSITORIES: Partial> = + typed.repositories; +export const METADATA: GithubData['metadata'] = typed.metadata; diff --git a/src/utils/testsSetup.ts b/src/utils/testsSetup.ts index cb2c6b3..3b660d3 100644 --- a/src/utils/testsSetup.ts +++ b/src/utils/testsSetup.ts @@ -1,12 +1,9 @@ import '@testing-library/jest-dom'; import { vi } from 'vitest'; -import { - REPOSITORY_INFO as STUB_REPOSITORY_INFO, - README_CONTENT as STUB_README_CONTENT, -} from 'mocks/mockGithubData'; +import { REPOSITORIES as STUB_REPOSITORIES, METADATA as STUB_METADATA } from 'mocks/mockGithubData'; vi.mock('utils/githubData', () => ({ - REPOSITORY_INFO: STUB_REPOSITORY_INFO, - README_CONTENT: STUB_README_CONTENT, + REPOSITORIES: STUB_REPOSITORIES, + METADATA: STUB_METADATA, })); From e3498f5345001875d23dad5decbfdb0e21351f3f Mon Sep 17 00:00:00 2001 From: Tenemo Date: Sat, 18 Oct 2025 12:12:17 +0200 Subject: [PATCH 2/8] fetchGithubData avoids unnecessary request for main branch if master exists --- src/types/github-data.ts | 2 +- src/utils/fetchGithubData.ts | 78 ++++++++++++++++++++---------------- src/utils/testsSetup.ts | 5 ++- 3 files changed, 49 insertions(+), 36 deletions(-) diff --git a/src/types/github-data.ts b/src/types/github-data.ts index b33ddda..b44b897 100644 --- a/src/types/github-data.ts +++ b/src/types/github-data.ts @@ -3,7 +3,7 @@ export type RepositoryInfo = { description: string; topics?: string[]; createdDatetime: string; - lastCommitDatetime: string; // must always be present + lastCommitDatetime: string; license?: string; readme_content: string; }; diff --git a/src/utils/fetchGithubData.ts b/src/utils/fetchGithubData.ts index 630bad5..216dba4 100644 --- a/src/utils/fetchGithubData.ts +++ b/src/utils/fetchGithubData.ts @@ -121,50 +121,60 @@ async function getPackageJsonLicenseFromMaster( owner: string, repo: string, ): Promise { - // Spec: license from package.json on master HEAD commit, if present. - // Fallback: if 'master' isn't available, try 'main'. - const candidateBranches = ['master', 'main'] as const; + // Spec: Read license from package.json on master HEAD. + // Only try 'main' if fetching from 'master' fails (e.g., 404). - for (const branch of candidateBranches) { + const extract = (raw: string): string | undefined => { try { - const raw = await fetchText( - `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/package.json`, - ); - try { - const pkg = JSON.parse(raw) as unknown; + 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 ( - pkg && - typeof pkg === 'object' && - 'license' in (pkg as Record) + lic && + typeof lic === 'object' && + 'type' in (lic as Record) ) { - const lic = (pkg as Record).license; - if (typeof lic === 'string') return lic; - if ( - lic && - typeof lic === 'object' && - 'type' in (lic as Record) - ) { - const typeVal = (lic as Record).type; - if (typeof typeVal === 'string') return typeVal; - } + const typeVal = (lic as Record).type; + if (typeof typeVal === 'string') return typeVal; } - // 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; - } + } + // 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 try next branch } } catch { - // package.json not found or inaccessible on this branch; try next + // ignore JSON parsing errors; treat as no license + } + return undefined; + }; + + // Try master + try { + const raw = await fetchText( + `https://raw.githubusercontent.com/${owner}/${repo}/master/package.json`, + ); + return extract(raw); + } catch { + // If master not available, try main + try { + const raw = await fetchText( + `https://raw.githubusercontent.com/${owner}/${repo}/main/package.json`, + ); + return extract(raw); + } catch { + return undefined; } } - return undefined; } async function getLastCommitDatetime( diff --git a/src/utils/testsSetup.ts b/src/utils/testsSetup.ts index 3b660d3..aa014a6 100644 --- a/src/utils/testsSetup.ts +++ b/src/utils/testsSetup.ts @@ -1,7 +1,10 @@ import '@testing-library/jest-dom'; import { vi } from 'vitest'; -import { REPOSITORIES as STUB_REPOSITORIES, METADATA as STUB_METADATA } from 'mocks/mockGithubData'; +import { + REPOSITORIES as STUB_REPOSITORIES, + METADATA as STUB_METADATA, +} from 'mocks/mockGithubData'; vi.mock('utils/githubData', () => ({ REPOSITORIES: STUB_REPOSITORIES, From c1279daa2a374f9b6f29e11ef603072d936a9f33 Mon Sep 17 00:00:00 2001 From: Tenemo Date: Sat, 18 Oct 2025 12:26:07 +0200 Subject: [PATCH 3/8] githubData renaming properties for consistency; using mocks everywhere in tests --- .../Projects/ProjectCard/ProjectCard.spec.tsx | 46 ----------- .../Projects/ProjectCard/ProjectCard.tsx | 4 +- .../Projects/ProjectItem/ProjectItem.spec.tsx | 10 --- .../Projects/ProjectItem/ProjectItem.tsx | 4 +- .../ProjectMarkdown/ProjectMarkdown.tsx | 4 +- src/mocks/mockGithubData.ts | 76 +++++++------------ src/routes/contact.tsx | 6 +- src/routes/index.tsx | 19 ++--- src/routes/project-item.tsx | 4 +- src/routes/projects.tsx | 6 +- src/types/{github-data.ts => githubData.d.ts} | 5 ++ src/types/temp-githubdata.d.ts | 6 -- src/utils/githubData.ts | 10 +-- src/utils/testsSetup.ts | 4 +- 14 files changed, 59 insertions(+), 145 deletions(-) rename src/types/{github-data.ts => githubData.d.ts} (76%) delete mode 100644 src/types/temp-githubdata.d.ts diff --git a/src/features/Projects/ProjectCard/ProjectCard.spec.tsx b/src/features/Projects/ProjectCard/ProjectCard.spec.tsx index f600b27..af64b35 100644 --- a/src/features/Projects/ProjectCard/ProjectCard.spec.tsx +++ b/src/features/Projects/ProjectCard/ProjectCard.spec.tsx @@ -6,52 +6,6 @@ import ProjectCard from './ProjectCard'; import styles from './projectCard.module.scss'; import { renderWithProviders } from 'utils/testUtils'; -// Provide a default export to align with JSON default import usage in code -vi.mock('../../../../temp/githubData', () => ({ - default: { - REPOSITORY_INFO: { - 'test-project': { - name: 'test-project', - description: 'Test description', - }, - 'img-test': { - name: 'img-test', - description: 'Image test', - createdDatetime: '2023-09-12T08:00:00.000Z', - }, - 'video-test': { - name: 'video-test', - description: 'Video test', - createdDatetime: '2022-01-20T15:30:00.000Z', - }, - 'right-test': { - name: 'right-test', - description: 'Right image test', - createdDatetime: '2024-06-01T00:00:00.000Z', - }, - 'custom-repo': { - name: 'custom-repo', - description: 'Custom repo test', - createdDatetime: '2020-12-31T23:59:59.000Z', - }, - 'cached-repo': { - name: 'cached-repo', - description: 'Cached repo', - createdDatetime: '2019-03-10T10:00:00.000Z', - }, - 'link-test': { - name: 'link-test', - description: 'Link test', - createdDatetime: '2018-07-07T07:07:07.000Z', - }, - 'epoch-test': { - name: 'epoch-test', - description: 'Epoch fallback', - createdDatetime: '1970-01-01T00:00:00.000Z', - }, - }, - }, -})); describe('ProjectCard', () => { beforeEach(() => { diff --git a/src/features/Projects/ProjectCard/ProjectCard.tsx b/src/features/Projects/ProjectCard/ProjectCard.tsx index b3af4a4..70c8d96 100644 --- a/src/features/Projects/ProjectCard/ProjectCard.tsx +++ b/src/features/Projects/ProjectCard/ProjectCard.tsx @@ -8,7 +8,7 @@ import { TECHNOLOGIES } from '../technologies'; import styles from './projectCard.module.scss'; import ProjectTechnologies from './ProjectTechnologies/ProjectTechnologies'; -import { REPOSITORIES } from 'utils/githubData'; +import { repositoriesData } from 'utils/githubData'; type ProjectCardProps = { projectPreview: string; @@ -26,7 +26,7 @@ const ProjectCard = ({ repoName, }: ProjectCardProps): React.JSX.Element => { const githubRepository = repoName ?? project; - const repositoryInfo = REPOSITORIES[githubRepository]; + const repositoryInfo = repositoriesData[githubRepository]; const createdIso = repositoryInfo?.createdDatetime; const createdLabel = createdIso ? format(new Date(createdIso), 'MMMM yyyy') diff --git a/src/features/Projects/ProjectItem/ProjectItem.spec.tsx b/src/features/Projects/ProjectItem/ProjectItem.spec.tsx index 45f2cec..35404dc 100644 --- a/src/features/Projects/ProjectItem/ProjectItem.spec.tsx +++ b/src/features/Projects/ProjectItem/ProjectItem.spec.tsx @@ -5,16 +5,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import ProjectItem from './ProjectItem'; import { renderWithProviders } from 'utils/testUtils'; -// The app imports JSON via default import, so the mock must provide a default export -vi.mock('../../../../temp/githubData', () => ({ - default: { - README_CONTENT: { - 'test-repo': '# Test Readme Content', - 'cached-project': '# Cached Readme Content', - 'new-content-project': '# New Readme Content', - }, - }, -})); const mockUseParams = vi.fn().mockReturnValue({ repo: 'test-repo' }); vi.mock('react-router', async () => { diff --git a/src/features/Projects/ProjectItem/ProjectItem.tsx b/src/features/Projects/ProjectItem/ProjectItem.tsx index 51fca88..0fc4585 100644 --- a/src/features/Projects/ProjectItem/ProjectItem.tsx +++ b/src/features/Projects/ProjectItem/ProjectItem.tsx @@ -6,7 +6,7 @@ import { Link, useParams } from 'react-router'; import styles from './projectItem.module.scss'; import ProjectMarkdown from './ProjectMarkdown/ProjectMarkdown'; -import { REPOSITORIES } from 'utils/githubData'; +import { repositoriesData } from 'utils/githubData'; const ProjectItemDetails: React.FC = (): React.JSX.Element => { const { repo } = useParams<{ repo: string }>(); @@ -19,7 +19,7 @@ const ProjectItemDetails: React.FC = (): React.JSX.Element => { ); } - const markdown = REPOSITORIES[repo]?.readme_content ?? ''; + const markdown = repositoriesData[repo]?.readme_content ?? ''; return (
diff --git a/src/features/Projects/ProjectItem/ProjectMarkdown/ProjectMarkdown.tsx b/src/features/Projects/ProjectItem/ProjectMarkdown/ProjectMarkdown.tsx index 6ad2583..1c44d21 100644 --- a/src/features/Projects/ProjectItem/ProjectMarkdown/ProjectMarkdown.tsx +++ b/src/features/Projects/ProjectItem/ProjectMarkdown/ProjectMarkdown.tsx @@ -10,7 +10,7 @@ import remarkGfm from 'remark-gfm'; import styles from './projectMarkdown.module.scss'; -import { REPOSITORIES } from 'utils/githubData'; +import { repositoriesData } from 'utils/githubData'; const OWNER = 'tenemo'; const BRANCH = 'master'; @@ -26,7 +26,7 @@ const ProjectMarkdown = ({ markdown, repo, }: ProjectMarkdownProps): React.JSX.Element => { - const createdIso = REPOSITORIES[repo]?.createdDatetime; + const createdIso = repositoriesData[repo]?.createdDatetime; const createdLabel = createdIso ? format(new Date(createdIso), 'MMMM yyyy') : undefined; diff --git a/src/mocks/mockGithubData.ts b/src/mocks/mockGithubData.ts index 016258a..c092c5c 100644 --- a/src/mocks/mockGithubData.ts +++ b/src/mocks/mockGithubData.ts @@ -1,85 +1,63 @@ -import type { GithubData } from 'types/github-data'; - -const baseInfo = { - lastCommitDatetime: '2025-01-02T00:00:00.000Z', - readme_content: '# Placeholder', -}; +import type { GithubData } from 'types/githubData'; const data: GithubData = { metadata: { fetchedDatetime: '2025-01-01T00:00:00.000Z' }, repositories: { - 'test-project': { - name: 'test-project', - description: 'Test description', - topics: ['react', 'typescript'], - createdDatetime: '2020-05-01T12:00:00.000Z', - ...baseInfo, + 'test-repo': { + name: 'test-repo', + description: 'Test Readme Content', + createdDatetime: '2020-01-01T00:00:00.000Z', + lastCommitDatetime: '2025-01-02T00:00:00.000Z', + readme_content: '# Test Readme Content', + }, + 'cached-project': { + name: 'cached-project', + description: 'Cached Readme Content', + createdDatetime: '2020-01-01T00:00:00.000Z', + lastCommitDatetime: '2025-01-02T00:00:00.000Z', + readme_content: '# Cached Readme Content', }, 'img-test': { name: 'img-test', description: 'Image test', - topics: ['images'], createdDatetime: '2023-09-12T08:00:00.000Z', - ...baseInfo, + lastCommitDatetime: '2025-01-02T00:00:00.000Z', + readme_content: '# Placeholder', + topics: ['images'], }, 'video-test': { name: 'video-test', description: 'Video test', - topics: ['video'], createdDatetime: '2022-01-20T15:30:00.000Z', - ...baseInfo, + lastCommitDatetime: '2025-01-02T00:00:00.000Z', + readme_content: '# Placeholder', }, 'right-test': { name: 'right-test', description: 'Right image test', - topics: ['ui'], - createdDatetime: '2018-07-15T00:00:00.000Z', - ...baseInfo, - }, - 'custom-repo': { - name: 'custom-repo', - description: 'Custom repo test', - createdDatetime: '2022-03-05T08:15:00.000Z', - ...baseInfo, - }, - 'cached-repo': { - name: 'cached-repo', - description: 'Cached repo', - topics: [], - createdDatetime: '2017-01-01T00:00:00.000Z', - ...baseInfo, + createdDatetime: '2024-06-01T00:00:00.000Z', + lastCommitDatetime: '2025-01-02T00:00:00.000Z', + readme_content: '# Placeholder', }, 'link-test': { name: 'link-test', description: 'Link test', createdDatetime: '2018-07-07T07:07:07.000Z', - ...baseInfo, + lastCommitDatetime: '2025-01-02T00:00:00.000Z', + readme_content: '# Placeholder', }, 'epoch-test': { name: 'epoch-test', description: 'Epoch fallback', createdDatetime: '1970-01-01T00:00:00.000Z', - ...baseInfo, - }, - 'test-repo': { - name: 'test-repo', - description: 'Test Readme Content', - createdDatetime: '2020-01-01T00:00:00.000Z', - ...baseInfo, - readme_content: '# Test Readme Content', - }, - 'cached-project': { - name: 'cached-project', - description: 'Cached Readme Content', - createdDatetime: '2020-01-01T00:00:00.000Z', - ...baseInfo, - readme_content: '# Cached Readme Content', + lastCommitDatetime: '2025-01-02T00:00:00.000Z', + readme_content: '# Placeholder', }, 'new-content-project': { name: 'new-content-project', description: 'New Readme Content', createdDatetime: '2020-01-01T00:00:00.000Z', - ...baseInfo, + lastCommitDatetime: '2025-01-02T00:00:00.000Z', readme_content: '# New Readme Content', }, }, diff --git a/src/routes/contact.tsx b/src/routes/contact.tsx index 39e8029..62a1447 100644 --- a/src/routes/contact.tsx +++ b/src/routes/contact.tsx @@ -11,7 +11,7 @@ import { } from 'app/appConstants'; import Contact from 'features/Contact/Contact'; import { getImageSize } from 'utils/getImageSize'; -import { REPOSITORIES } from 'utils/githubData'; +import { repositoriesData } from 'utils/githubData'; const breadcrumbList: BreadcrumbList = { '@type': 'BreadcrumbList', @@ -66,8 +66,8 @@ export const meta: MetaFunction = () => { breadcrumb: { '@id': 'https://piech.dev/contact/#breadcrumb' }, primaryImageOfPage: { '@id': 'https://piech.dev/contact/#image' }, image: { '@id': 'https://piech.dev/contact/#image' }, - datePublished: REPOSITORIES['piech.dev']?.createdDatetime, - dateModified: REPOSITORIES['piech.dev']?.lastCommitDatetime, + datePublished: repositoriesData['piech.dev']?.createdDatetime, + dateModified: repositoriesData['piech.dev']?.lastCommitDatetime, }; const graph: Graph = { diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 3582823..4c29213 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -19,7 +19,7 @@ import About from 'features/About/About'; import { PROJECTS } from 'features/Projects/projectsList'; import { TECHNOLOGIES } from 'features/Projects/technologies'; import { getImageSize } from 'utils/getImageSize'; -import { REPOSITORIES } from 'utils/githubData'; +import { repositoriesData } from 'utils/githubData'; const alumniOf: EducationalOrganization = { '@type': 'EducationalOrganization', @@ -66,7 +66,7 @@ export const PERSON: Person = { name: 'Piotr Piech', givenName: 'Piotr', familyName: 'Piech', - image: 'https://piech.dev/media/projects/og_images/piotr.jpg', + image: { '@id': PIOTR_IMAGE_ID }, email: 'piotr@piech.dev', description: 'Engineering Manager & Software Architect specializing in full-stack web development.', @@ -116,8 +116,8 @@ export const WEBSITE: WebSite = { author: { '@id': PERSON_ID }, publisher: { '@id': PERSON_ID }, copyrightHolder: { '@id': PERSON_ID }, - datePublished: REPOSITORIES['piech.dev']?.createdDatetime, - dateModified: REPOSITORIES['piech.dev']?.lastCommitDatetime, + datePublished: repositoriesData['piech.dev']?.createdDatetime, + dateModified: repositoriesData['piech.dev']?.lastCommitDatetime, }; export const meta: MetaFunction = () => { @@ -156,18 +156,13 @@ export const meta: MetaFunction = () => { mainEntity: { '@id': PERSON_ID }, primaryImageOfPage: { '@id': PIOTR_IMAGE_ID }, image: { '@id': PIOTR_IMAGE_ID }, - datePublished: REPOSITORIES['piech.dev']?.createdDatetime, - dateModified: REPOSITORIES['piech.dev']?.lastCommitDatetime, - }; - - const personNode: Person = { - ...PERSON, - image: { '@id': PIOTR_IMAGE_ID }, + datePublished: repositoriesData['piech.dev']?.createdDatetime, + dateModified: repositoriesData['piech.dev']?.lastCommitDatetime, }; const graph: Graph = { '@context': 'https://schema.org', - '@graph': [WEBSITE, aboutWebPage, personNode, portrait], + '@graph': [WEBSITE, aboutWebPage, PERSON, portrait], }; return [ diff --git a/src/routes/project-item.tsx b/src/routes/project-item.tsx index 981d86f..4cb62f5 100644 --- a/src/routes/project-item.tsx +++ b/src/routes/project-item.tsx @@ -18,11 +18,11 @@ import { import ProjectItem from 'features/Projects/ProjectItem/ProjectItem'; import { PROJECTS } from 'features/Projects/projectsList'; import { getImageSize } from 'utils/getImageSize'; -import { REPOSITORIES } from 'utils/githubData'; +import { repositoriesData } from 'utils/githubData'; export const meta: MetaFunction = (args) => { const repo = args.params.repo ?? ''; - const info = REPOSITORIES[repo]; + const info = repositoriesData[repo]; const title = `${repo} | piech.dev`; const desc = info?.description ?? diff --git a/src/routes/projects.tsx b/src/routes/projects.tsx index fb302b5..07664ec 100644 --- a/src/routes/projects.tsx +++ b/src/routes/projects.tsx @@ -20,7 +20,7 @@ import { import Projects from 'features/Projects/Projects'; import { PROJECTS } from 'features/Projects/projectsList'; import { getImageSize } from 'utils/getImageSize'; -import { REPOSITORIES } from 'utils/githubData'; +import { repositoriesData } from 'utils/githubData'; const projectsItemList: ItemList = { '@type': 'ItemList', @@ -99,8 +99,8 @@ export const meta: MetaFunction = () => { mainEntity: projectsItemList, primaryImageOfPage: { '@id': 'https://piech.dev/projects/#main-image' }, image: { '@id': 'https://piech.dev/projects/#main-image' }, - datePublished: REPOSITORIES['piech.dev']?.createdDatetime, - dateModified: REPOSITORIES['piech.dev']?.lastCommitDatetime, + datePublished: repositoriesData['piech.dev']?.createdDatetime, + dateModified: repositoriesData['piech.dev']?.lastCommitDatetime, }; const graph: Graph = { diff --git a/src/types/github-data.ts b/src/types/githubData.d.ts similarity index 76% rename from src/types/github-data.ts rename to src/types/githubData.d.ts index b44b897..3b2236f 100644 --- a/src/types/github-data.ts +++ b/src/types/githubData.d.ts @@ -12,3 +12,8 @@ export type GithubData = { metadata?: { fetchedDatetime: string }; repositories: Partial>; }; + +declare module '../../temp/githubData.json' { + const value: GithubData; + export default value; +} diff --git a/src/types/temp-githubdata.d.ts b/src/types/temp-githubdata.d.ts deleted file mode 100644 index 7623f0e..0000000 --- a/src/types/temp-githubdata.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { GithubData } from './github-data'; - -declare module '../../temp/githubData.json' { - const value: GithubData; - export default value; -} diff --git a/src/utils/githubData.ts b/src/utils/githubData.ts index 950372f..a513e51 100644 --- a/src/utils/githubData.ts +++ b/src/utils/githubData.ts @@ -8,11 +8,9 @@ /* eslint-disable */ import githubData from '../../temp/githubData.json'; -import type { GithubData, RepositoryInfo } from 'types/github-data'; +import type { GithubData, RepositoryInfo } from 'types/githubData'; /* eslint-enable */ -const typed: GithubData = githubData as unknown as GithubData; - -export const REPOSITORIES: Partial> = - typed.repositories; -export const METADATA: GithubData['metadata'] = typed.metadata; +export const repositoriesData: Partial> = + githubData.repositories; +export const metadata: GithubData['metadata'] = githubData.metadata; diff --git a/src/utils/testsSetup.ts b/src/utils/testsSetup.ts index aa014a6..ae6f297 100644 --- a/src/utils/testsSetup.ts +++ b/src/utils/testsSetup.ts @@ -7,6 +7,6 @@ import { } from 'mocks/mockGithubData'; vi.mock('utils/githubData', () => ({ - REPOSITORIES: STUB_REPOSITORIES, - METADATA: STUB_METADATA, + repositoriesData: STUB_REPOSITORIES, + metadata: STUB_METADATA, })); From 6c0f0361b532237ecf5f629b7f85276b78a074eb Mon Sep 17 00:00:00 2001 From: Tenemo Date: Sat, 18 Oct 2025 12:48:18 +0200 Subject: [PATCH 4/8] robots.txt tweak --- public/robots.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/robots.txt b/public/robots.txt index ee7e117..c2a49f4 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,2 +1,2 @@ -User-agent: * -Disallow: +User-agent: * +Allow: / From 74fcd94e1208d4e6190b4cdccf1b02c85f0df92d Mon Sep 17 00:00:00 2001 From: Tenemo Date: Sun, 19 Oct 2025 06:31:59 +0200 Subject: [PATCH 5/8] sitemap generation; readme explanations update --- .vscode/settings.json | 1 + README.md | 34 +++++++++++ package-lock.json | 38 ++++++++++++ package.json | 3 +- src/utils/generateSitemap.ts | 110 +++++++++++++++++++++++++++++++++++ 5 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/utils/generateSitemap.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 9d12604..250c8f5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -58,6 +58,7 @@ "fssync", "gitlens", "hrefs", + "HTMX", "jpoissonnier", "julr", "jvitor", diff --git a/README.md b/README.md index 1530a1a..22846f4 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,37 @@ My personal page. Over time it turned into a complex project itself: it supports - Thanks to the above and a ton of other optimizations, all routes, including the heaviest /projects route, score a perfect 100/100/100/100 on [Google Lighthouse](https://pagespeed.web.dev/) mobile, which throttles to a slow 4G network connection. Lighthouse results + +## Post-build scripts + +As of 2025, if you want to use React for generating static HTML sites and you don't rely on all-batteries-included frameworks/services, the ecosystem support is quite poor. React-router v7 with its framework mode and pre-rendering support changed the situation for the better. Still, it is assumed you will serve the .html static assets and then hydrate the page with JavaScript. If you want to have a true zero-JS output, you have do some of the wiring yourself. Moreover, most popular NPM packages for inlining CSS inline it into each html element's tags, which doesn't support media queries. + +This resulted in the project using the following post-build scripts: + +1. + +### Why use React at all? + +One might ask, if you don't want to bundle any JavaScript, why bother using React at all, just write HTML & CSS or use tools meant for the purpose. + +First of all, I want a framework to provide me with a reusable component system and state management logic during the pre-render step and I find React easy to use for this purpose. + +There are most likely better libraries/frameworks to do what I'm doing, but the second simple reason why I went with React is that I'm just comfortable with it and I wanted to save time. Did I end up saving time writing all this post-rendering logic and fighting the build tools not to require JS? Probably not, but I embrace the sunk-cost fallacy for my personal site and plan to continue with React :) + +Another reason is that I initially intended for the whole page to be hydrated - and I might still start doing that if I need more interactivity - but I realized all that I'm doing can be served without any JavaScript. It's unlikely I'll do it, but it's good to always have that option: with minimal changes to the project, this static-content webpage can be transformed into a client-side React app. + +## Development + +Notes for myself (Piotr) to follow when making changes to the project. At the time of writing it seems a bit redundant, but there's a nonzero chance that future Piotr will be very grateful. + +### Adding a new route + +Steps to follow when adding a new route to the app: + +1. + +### Adding a new project + +Steps + +1. diff --git a/package-lock.json b/package-lock.json index 566fc99..0a26961 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "rollup-plugin-visualizer": "^6.0.5", "sass-embedded": "^1.93.2", "schema-dts": "^1.1.5", + "sitemap": "^8.0.0", "stylelint": "^16.25.0", "stylelint-config-recommended": "^17.0.0", "stylelint-config-recommended-scss": "^16.0.2", @@ -3520,6 +3521,16 @@ "@types/react": "*" } }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -18672,6 +18683,33 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sitemap": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-8.0.0.tgz", + "integrity": "sha512-+AbdxhM9kJsHtruUF39bwS/B0Fytw6Fr1o4ZAIAEqA6cke2xcoO2GleBw9Zw7nRzILVEgz7zBM5GiTJjie1G9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.2.4" + }, + "bin": { + "sitemap": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "dev": true, + "license": "MIT" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/package.json b/package.json index 9b498cd..6139916 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dev": "react-router dev", "prebuild": "node --experimental-strip-types src/utils/fetchGithubData.ts && npm run lint && npm run tsc && npm test", "build": "react-router build", - "postbuild": "node --experimental-strip-types src/utils/transformImagePaths.ts && node --experimental-strip-types src/utils/inlineCss.ts && rimraf dist/server && rimraf dist/client/assets && rimraf --glob \"dist/client/**/*.js\"", + "postbuild": "node --experimental-strip-types src/utils/transformImagePaths.ts && node --experimental-strip-types src/utils/inlineCss.ts && rimraf dist/server && rimraf dist/client/assets && rimraf --glob \"dist/client/**/*.js\" && node --experimental-strip-types src/utils/generateSitemap.ts", "prebuild:skip": "node --experimental-strip-types src/utils/fetchGithubData.ts", "build:skip": "react-router build", "postbuild:skip": "npm run postbuild", @@ -106,6 +106,7 @@ "rollup-plugin-visualizer": "^6.0.5", "sass-embedded": "^1.93.2", "schema-dts": "^1.1.5", + "sitemap": "^8.0.0", "stylelint": "^16.25.0", "stylelint-config-recommended": "^17.0.0", "stylelint-config-recommended-scss": "^16.0.2", diff --git a/src/utils/generateSitemap.ts b/src/utils/generateSitemap.ts new file mode 100644 index 0000000..b856433 --- /dev/null +++ b/src/utils/generateSitemap.ts @@ -0,0 +1,110 @@ +// vite-plugin-sitemap doesn't work with URLs with dots, +// cuts off /piech.dev and aliases.sh to /piech and /aliases + +import 'dotenv/config'; +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { Readable } from 'node:stream'; + +import { SitemapStream, streamToPromise } from 'sitemap'; + +// Minimal type for the link items we push into the sitemap stream. +// Avoid importing non-existent types from 'sitemap' across versions. +type SitemapLink = { + url: string; + lastmod?: Date | string; + changefreq?: + | 'always' + | 'hourly' + | 'daily' + | 'weekly' + | 'monthly' + | 'yearly' + | 'never'; + priority?: number; +}; + +const OUT_DIR = path.resolve('dist/client'); + +const SITE_URL = 'https://piech.dev'.replace(/\/+$/, ''); + +const IGNORE_FILE_NAMES = new Set(['404.html', '__spa-fallback.html']); + +async function getHtmlFiles(dir: string): Promise { + // Dynamic path is expected here; safe in our controlled build output directory. + // eslint-disable-next-line security/detect-non-literal-fs-filename + const entries = await fs.readdir(dir, { withFileTypes: true }); + const found: string[] = []; + for (const entry of entries) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === 'assets') continue; // ignore asset folder + found.push(...(await getHtmlFiles(full))); + } else if ( + entry.isFile() && + entry.name.endsWith('.html') && + !IGNORE_FILE_NAMES.has(entry.name) + ) { + found.push(full); + } + } + return found; +} + +// Convert "dist/client/foo/index.html" -> "/foo/" +// Convert "dist/client/bar.html" -> "/bar" +function toRoute(fromDir: string, file: string): string { + const rel = path.relative(fromDir, file).split(path.sep).join('/'); // POSIX style + if (rel === 'index.html') return '/'; + if (rel.endsWith('/index.html')) + return `/${rel.slice(0, -'index.html'.length)}`; + return `/${rel.replace(/\.html$/, '')}`; +} + +async function main(): Promise { + // Ensure build exists + await fs.access(OUT_DIR); + + const files = await getHtmlFiles(OUT_DIR); + + const links: SitemapLink[] = []; + for (const file of files) { + const url = toRoute(OUT_DIR, file); + + // skip internal/temporary routes if any slipped through + if (url.includes('/__') || url.includes('/_internal')) continue; + + // Dynamic path is expected in our build output. + // eslint-disable-next-line security/detect-non-literal-fs-filename + const stat = await fs.stat(file); + links.push({ + url, + lastmod: stat.mtime, // Date is accepted + // small, sensible defaults + changefreq: url === '/' ? 'weekly' : 'monthly', + priority: + url === '/' + ? 1.0 + : url.split('/').filter(Boolean).length === 1 + ? 0.8 + : 0.6, + }); + } + + const sm = new SitemapStream({ hostname: SITE_URL }); + const xml = ( + await streamToPromise(Readable.from(links).pipe(sm)) + ).toString(); + + const outFile = path.join(OUT_DIR, 'sitemap.xml'); + await fs.writeFile(outFile, xml, 'utf8'); + // Avoid non-string interpolation per eslint rule + console.log( + `sitemap.xml written (${String(links.length)} URLs): ${outFile}`, + ); +} + +main().catch((err: unknown) => { + console.error('sitemap generation failed:', err); + process.exitCode = 1; +}); From 4bf4c90cbe7316912e84f621ac8092796ccb82fb Mon Sep 17 00:00:00 2001 From: Tenemo Date: Sun, 19 Oct 2025 07:24:18 +0200 Subject: [PATCH 6/8] readme postbuild steps + SOPs for adding routes/projects; added on-hover shadow to project cards --- README.md | 56 ++++++++++++++----- .../ProjectCard/projectCard.module.scss | 4 ++ 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 22846f4..0130c7b 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,32 @@ [![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 tags for each project page, including individual og:image tags with sizes. All routes have appropriate JSON-LD objects with relevant information. +My personal page. Over time it turned into a complex project itself: -Lighthouse results +- Despite being written in React, the whole page is pre-rendered and served with zero JavaScript! +- It supports loading all projects' information directly from GitHub. +- GitHub's markdown from the projects' readmes is rendered within the page, including media. +- Includes dynamic \ tags for each project page, including individual og:image tags with sizes. +- All routes have appropriate JSON-LD objects with all relevant information, even if Google or other search engines currently don't support some of them. +- Thanks to the above and a bunch of other optimizations, all routes, including the heaviest /projects route, score a perfect 100/100/100/100 on [Google Lighthouse](https://pagespeed.web.dev/) mobile, which throttles to a slow 4G network connection. -## Dynamic project list pulled from GitHub-based +_Not a single word of this readme was written with AI. Letting you know just in case you don't enjoy reading AI-generated documentation._ -- 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. +Projects page of piech.dev + +## GitHub-based personal projects browser + +- The projects/ page is managed via minimal configuration, just based on repository names which are then fetched during the pre-build step. 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 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 +## Pre-rendering React with zero JavaScript served to the user - The whole site, including all project routes, is pre-rendered with React Router in framework mode into HTML. -- The site ships without ANY client-side JavaScript despite being built in React. -- For performance reasons, the whole CSS is inlined into HTML, as there is so little of it and it significantly sped up page load times and it eliminates the awful Flash of Unstyled Content no matter the connection speed. +- The site ships without ANY client-side JavaScript despite being built in React. The build tools have been fighting me on this, so I'm traversing the build directory and deleting all JavaScript even if it wouldn't be linked to, "just to be safe" :) +- For performance reasons, all CSS is inlined into HTML, as there is so little of it (in the grand scheme of things and network payloads). It significantly sped up page load times and it eliminates the awful Flash of Unstyled Content no matter the user's connection speed. - Images are utilizing Netlify Image CDN to speed up their load times and to avoid serving oversized images. -- Thanks to the above and a ton of other optimizations, all routes, including the heaviest /projects route, score a perfect 100/100/100/100 on [Google Lighthouse](https://pagespeed.web.dev/) mobile, which throttles to a slow 4G network connection. Lighthouse results @@ -29,9 +37,14 @@ My personal page. Over time it turned into a complex project itself: it supports As of 2025, if you want to use React for generating static HTML sites and you don't rely on all-batteries-included frameworks/services, the ecosystem support is quite poor. React-router v7 with its framework mode and pre-rendering support changed the situation for the better. Still, it is assumed you will serve the .html static assets and then hydrate the page with JavaScript. If you want to have a true zero-JS output, you have do some of the wiring yourself. Moreover, most popular NPM packages for inlining CSS inline it into each html element's tags, which doesn't support media queries. -This resulted in the project using the following post-build scripts: +This resulted in the project using the following post-build steps: -1. +1. Transforming image paths to use Netlify Image CDN, scans all .html files and looks for images matching the criteria (it doesn't touch the og:image assets, for instance). +2. Inlining all CSS. Finds all .html files, resolves the files and dumps all their content into a single \