cleanup
All checks were successful
Build and Deploy / build (push) Successful in 1m45s

This commit is contained in:
Joakim Repomaa
2026-02-19 21:51:10 +02:00
parent 782f46f69f
commit c8044259cc
28 changed files with 442 additions and 502 deletions

View File

@@ -1,6 +1,5 @@
<script>
let { children } = $props();
import '../../app.css';
</script>
<div class="font-mono bg-background text-foreground">

View File

@@ -34,7 +34,7 @@
<a
href="/joakim-repomaa-cv.pdf"
download
class="no-print fixed top-0 right-0 w-24 h-24 z-50 group"
class="fixed top-0 right-0 w-24 h-24 z-50 group"
style="clip-path: polygon(0 0, 100% 0, 100% 100%);"
data-sveltekit-preload-data="off"
aria-label="Download PDF"
@@ -51,12 +51,3 @@
<Footer />
</main>
<style>
/* Ensure proper print styling */
@media print {
main {
background: white !important;
}
}
</style>

View File

@@ -0,0 +1,21 @@
import { loadAllContent } from '$lib/content/loader';
import { fetchContributedRepos, fetchGitHubProjects } from '$lib/github';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async () => {
const content = await loadAllContent();
const [ownProjects, contributions] = await Promise.all([
fetchGitHubProjects(content.profile.github),
fetchContributedRepos(content.profile.github),
]);
return {
profile: content.profile,
experience: content.experience,
education: content.education,
skills: content.skills,
ownProjects,
contributions,
};
};

View File

@@ -0,0 +1,14 @@
<script>
let { children } = $props();
</script>
<main class="min-h-screen bg-pdf-bg text-pdf-fg">
{@render children()}
</main>
<style>
@page {
size: A4;
margin: 15mm;
}
</style>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import PDFContent from '$lib/components/PDFContent.svelte';
let { data } = $props();
</script>
<svelte:head>
<title>{data.profile.name} — CV</title>
</svelte:head>
<PDFContent
profile={data.profile}
experience={data.experience}
education={data.education}
skills={data.skills}
ownProjects={data.ownProjects}
contributions={data.contributions}
/>

View File

@@ -1,24 +0,0 @@
import type { PageServerLoad } from './$types.js';
import { loadAllContent } from '$lib/content/loader.js';
import { fetchGitHubProjects, fetchContributedRepos } from '$lib/github.js';
export const prerender = true;
export const load: PageServerLoad = async () => {
const content = loadAllContent();
// Fetch GitHub projects and contributions in parallel
const [ownProjects, contributions] = await Promise.all([
fetchGitHubProjects(content.profile.github),
fetchContributedRepos(content.profile.github),
]);
return {
profile: content.profile,
experience: content.experience,
education: content.education,
skills: content.skills,
ownProjects: ownProjects.slice(0, 6),
contributions: contributions.slice(0, 5),
};
};

View File

@@ -1,5 +0,0 @@
<script>
let { children } = $props();
</script>
{@render children()}

View File

@@ -1,113 +1,10 @@
<script lang="ts">
import { onMount } from 'svelte';
import config from '$lib/content/config';
onMount(async () => {
const CMS = await import('@sveltia/cms');
CMS.init({
config: {
load_config_file: false,
media_folder: 'src/lib/media',
backend: {
name: 'gitea',
app_id: 'a046b53c-787a-4b76-bd3a-633221a38954',
repo: 'repomaa/cv',
},
collections: [
{
name: 'profile',
label: 'Profile',
format: 'json',
files: [
{
name: 'profile',
label: 'Profile',
file: 'src/lib/content/profile.json',
fields: [
{ name: 'name', label: 'Name', widget: 'string' },
{ name: 'title', label: 'Title', widget: 'string' },
{ name: 'email', label: 'Email', widget: 'string' },
{ name: 'phone', label: 'Phone', widget: 'string', required: false },
{ name: 'location', label: 'Location', widget: 'string' },
{ name: 'website', label: 'Website', widget: 'string', required: false },
{ name: 'github', label: 'GitHub', widget: 'string' },
{ name: 'avatar', label: 'Avatar', widget: 'image', required: false },
{
name: 'profilePicture',
label: 'Profile Picture (for PDF)',
widget: 'image',
required: false,
},
{ name: 'summary', label: 'Summary', widget: 'text' },
],
},
],
},
{
name: 'experience',
label: 'Experience',
format: 'json',
folder: 'src/lib/content/experience',
fields: [
{ name: 'company', label: 'Company', widget: 'string' },
{ name: 'position', label: 'Position', widget: 'string' },
{ name: 'location', label: 'Location', widget: 'string', required: false },
{ name: 'startDate', label: 'Start Date', widget: 'datetime', time_format: false },
{
name: 'endDate',
label: 'End Date',
widget: 'datetime',
time_format: false,
required: false,
},
{ name: 'description', label: 'Description', widget: 'markdown' },
{
name: 'technologies',
label: 'Technologies',
widget: 'list',
field: { label: 'Technology', widget: 'string' },
},
],
},
{
name: 'education',
label: 'Education',
format: 'json',
folder: 'src/lib/content/education',
fields: [
{ name: 'institution', label: 'Institution', widget: 'string' },
{ name: 'degree', label: 'Degree', widget: 'string' },
{ name: 'field', label: 'Field', widget: 'string', required: false },
{ name: 'location', label: 'Location', widget: 'string', required: false },
{ name: 'startDate', label: 'Start Date', widget: 'datetime', time_format: false },
{
name: 'endDate',
label: 'End Date',
widget: 'datetime',
time_format: false,
required: false,
},
{ name: 'description', label: 'Description', widget: 'markdown', required: false },
],
},
{
name: 'skills',
label: 'Skills',
format: 'json',
folder: 'src/lib/content/skills',
fields: [
{ name: 'category', label: 'Category', widget: 'string' },
{
name: 'items',
label: 'Skills',
widget: 'list',
field: { label: 'Skill', widget: 'string' },
},
],
},
],
},
});
CMS.init({ config });
});
</script>

View File

@@ -1,5 +1,6 @@
import type { RequestHandler } from './$types.js';
import type { LaunchOptions } from 'puppeteer';
import { dev } from '$app/environment';
import puppeteer from 'puppeteer';
import * as cheerio from 'cheerio';
import path from 'path';
@@ -37,7 +38,22 @@ const getLaunchOptions = (): LaunchOptions => {
return options;
};
export const GET: RequestHandler = async ({ url, fetch }) => {
/**
* Prepares the print page HTML and returns the URL to use for PDF generation.
* During dev server, returns the dev server URL directly.
* During build time, copies assets to temp dir and returns file:// URL.
*/
const preparePrintPageUrl = async (
requestUrl: URL,
fetch: typeof globalThis.fetch
): Promise<string> => {
// In dev mode, just use the dev server URL
if (dev) {
const devUrl = new URL('/print/', requestUrl);
return devUrl.toString();
}
// Build time: prepare files in temp directory
const tmpDir = await mkdtemp(path.join(tmpdir(), 'cv-pdf-genration-'));
const tmpFile = (url: string) => {
const filename = path.basename(url);
@@ -45,63 +61,70 @@ export const GET: RequestHandler = async ({ url, fetch }) => {
return tempFile;
};
try {
// Launch browser
const browser = await puppeteer.launch(getLaunchOptions());
const page = await browser.newPage();
const printResponse = await fetch('/print/');
const html = await printResponse.text();
const $ = cheerio.load(html);
const printResponse = await fetch('/print/');
const html = await printResponse.text();
const $ = cheerio.load(html);
const fileDownloads: Record<string, string> = {};
const fileDownloads: Record<string, string> = {};
$('script[src], link[rel="stylesheet"], img[src]').each((i, el) => {
if (el.tagName === 'link') {
const href = $(el).attr('href');
if (href) {
const tempFile = (fileDownloads[href] ||= tmpFile(href));
$(el).attr('href', `file://${tempFile}`);
}
} else {
const src = $(el).attr('src');
if (src) {
const tempFile = (fileDownloads[src] ||= tmpFile(src));
$(el).attr('src', `file://${tempFile}`);
}
$('script[src], link[rel="stylesheet"], img[src]').each((i, el) => {
if (el.tagName === 'link') {
const href = $(el).attr('href');
if (href) {
const tempFile = (fileDownloads[href] ||= tmpFile(href));
$(el).attr('href', `file://${tempFile}`);
}
});
$('style[src]').each((i, el) => {
} else {
const src = $(el).attr('src');
if (src) {
const tempFile = (fileDownloads[src] ||= tmpFile(src));
$(el).attr('src', `file://${tempFile}`);
}
});
$('style:not([src])').each((i, el) => {
const content = $(el).text();
$(el).text(
content.replaceAll(/(?<=url\(".+?)(?=")/g, (match) => {
const url = match[0];
const tempFile = (fileDownloads[url] ||= tmpFile(url));
return `file://${tempFile}`;
})
);
});
const dir = await opendir('.svelte-kit/output/client/_app/immutable/assets');
for await (const file of dir) {
await copyFile(path.join(file.parentPath, file.name), tmpFile(file.name));
}
});
const htmlFile = path.join(tmpDir, 'index.html');
await writeFile(htmlFile, $.root().html() ?? '');
$('style[src]').each((i, el) => {
const src = $(el).attr('src');
if (src) {
const tempFile = (fileDownloads[src] ||= tmpFile(src));
$(el).attr('src', `file://${tempFile}`);
}
});
$('style:not([src])').each((i, el) => {
const content = $(el).text();
$(el).text(
content.replaceAll(/(?<=url\(".+?)(?=")/g, (match) => {
const url = match[0];
const tempFile = (fileDownloads[url] ||= tmpFile(url));
return `file://${tempFile}`;
})
);
});
const dir = await opendir('.svelte-kit/output/client/_app/immutable/assets');
for await (const file of dir) {
await copyFile(path.join(file.parentPath, file.name), tmpFile(file.name));
}
const htmlFile = path.join(tmpDir, 'index.html');
await writeFile(htmlFile, $.root().html() ?? '');
return `file://${htmlFile}`;
};
export const GET: RequestHandler = async ({ url, fetch }) => {
try {
// Launch browser
const browser = await puppeteer.launch(getLaunchOptions());
const page = await browser.newPage();
const pageUrl = await preparePrintPageUrl(url, fetch);
// Navigate to the PDF page with increased timeout
// waitUntil: 'networkidle2' waits for 2 network connections to be idle
// This is more lenient than 'networkidle0' which waits for 0 connections
await page.goto(`file://${htmlFile}`, { waitUntil: 'networkidle2' });
await page.goto(pageUrl, { waitUntil: 'networkidle2' });
// Wait for fonts to load
await page.evaluateHandle('document.fonts.ready');

View File

@@ -1,24 +0,0 @@
import type { PageServerLoad } from './$types.js';
import { loadAllContent } from '$lib/content/loader.js';
import { fetchGitHubProjects, fetchContributedRepos } from '$lib/github.js';
export const prerender = true;
export const load: PageServerLoad = async () => {
const content = loadAllContent();
// Fetch GitHub projects and contributions at build time
const [ownProjects, contributions] = await Promise.all([
fetchGitHubProjects(content.profile.github),
fetchContributedRepos(content.profile.github),
]);
return {
profile: content.profile,
experience: content.experience,
education: content.education,
skills: content.skills,
ownProjects: ownProjects.slice(0, 6),
contributions: contributions.slice(0, 5),
};
};

View File

@@ -1,50 +0,0 @@
<script lang="ts">
import '../../app.css';
import PDFContent from '$lib/components/PDFContent.svelte';
let { data } = $props();
</script>
<svelte:head>
<title>{data.profile.name} — CV (Print Version)</title>
<meta name="description" content="Printable CV for {data.profile.name}" />
<!-- Print-specific styles -->
<style>
@page {
size: A4;
margin: 15mm;
}
@media print {
body {
background: white;
color: black;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
/* Explicitly hide skip link in PDF */
.skip-link,
.no-print {
display: none !important;
}
}
</style>
</svelte:head>
<main class="min-h-screen bg-pdf-bg">
<PDFContent
profile={data.profile}
experience={data.experience}
education={data.education}
skills={data.skills}
ownProjects={data.ownProjects}
contributions={data.contributions}
/>
</main>