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
39 changes: 23 additions & 16 deletions .github/workflows/generate-cv-pdf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ on:
push:
branches: [ main ]
paths-ignore:
- 'static/colin-moerbe-cv.pdf'
- 'static/colin-moerbe-cv-en.pdf'
- 'static/colin-moerbe-cv-de.pdf'
workflow_dispatch:

permissions:
Expand Down Expand Up @@ -45,7 +46,7 @@ jobs:
- name: Wait for preview server
run: |
for i in {1..30}; do
if curl -sSf http://127.0.0.1:4173/cv/ > /dev/null; then
if curl -sSf http://127.0.0.1:4173/en/cv/ > /dev/null && curl -sSf http://127.0.0.1:4173/de/cv/ > /dev/null; then
echo "Preview server is up"
exit 0
fi
Expand All @@ -55,7 +56,7 @@ jobs:
cat preview.log
exit 1

- name: Generate CV PDF
- name: Generate CV PDFs
run: bun run generate:cv-pdf

- name: Stop preview server
Expand All @@ -66,34 +67,40 @@ jobs:
- name: Install qpdf
run: sudo apt-get update && sudo apt-get install -y qpdf

- name: Normalize generated PDF
- name: Normalize generated PDFs
shell: bash
run: |
if [ -f static/colin-moerbe-cv.pdf ]; then
qpdf --linearize static/colin-moerbe-cv.pdf static/colin-moerbe-cv.pdf.normalized
mv static/colin-moerbe-cv.pdf.normalized static/colin-moerbe-cv.pdf
fi
for file in static/colin-moerbe-cv-en.pdf static/colin-moerbe-cv-de.pdf; do
if [ ! -f "$file" ]; then
echo "Missing artifact: $file"
exit 1
fi
qpdf --linearize "$file" "$file.normalized"
mv "$file.normalized" "$file"
done

- name: Check for PDF changes
id: pdf_paths
shell: bash
run: |
changed=$(git diff --name-only -- static/colin-moerbe-cv.pdf)
new=$(git ls-files --others --exclude-standard -- static/colin-moerbe-cv.pdf)
changed=$(git diff --name-only -- static/colin-moerbe-cv-en.pdf static/colin-moerbe-cv-de.pdf)
new=$(git ls-files --others --exclude-standard -- static/colin-moerbe-cv-en.pdf static/colin-moerbe-cv-de.pdf)

if [ -z "$changed" ] && [ -z "$new" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Create pull request for updated PDF
- name: Create pull request for updated PDFs
if: steps.pdf_paths.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v8
with:
commit-message: "chore: regenerate CV PDF"
title: "chore: regenerate CV PDF"
body: "Automated update of the CV PDF."
branch: "chore/update-cv-pdf"
commit-message: "chore: regenerate localized CV PDFs"
title: "chore: regenerate localized CV PDFs"
body: "Automated update of EN/DE CV PDFs."
branch: "chore/update-cv-pdfs"
delete-branch: true
add-paths: static/colin-moerbe-cv.pdf
add-paths: |
static/colin-moerbe-cv-en.pdf
static/colin-moerbe-cv-de.pdf
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ This repository contains my personal portfolio.
- **Runtime:** Bun
- **Hosting:** Netlify

## Bilingual Content Workflow (EN/DE)

- Content lives in:
- `src/lib/content/en/`
- `src/lib/content/de/`
- Filenames must be identical in both locales.
- Content needs to be updated in both locales.
- `bun run check:i18n` and `bun run generate:cv-pdf:local` should be run after updating content.

## Documentation

- [Setup & Development Guide](docs/setup.md)
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"preview": "vite preview",
"preview:ci": "vite preview --host 127.0.0.1 --port 4173",
"generate:cv-pdf": "bun scripts/generate-cv-pdf.mjs",
"generate:cv-pdf:local": "bun run build && bash -lc 'bun run preview:ci > /dev/null 2>&1 & PREVIEW_PID=$!; trap \"kill $PREVIEW_PID\" EXIT; READY=0; for i in {1..30}; do if curl -sSf http://127.0.0.1:4173/cv/ >/dev/null 2>&1; then READY=1; break; fi; sleep 1; done; if [ \"$READY\" -ne 1 ]; then echo \"Preview server did not become ready in time\"; exit 1; fi; bun run generate:cv-pdf'",
"generate:cv-pdf:local": "bun run build && bash -lc 'bun run preview:ci > /dev/null 2>&1 & PREVIEW_PID=$!; trap \"kill $PREVIEW_PID\" EXIT; READY=0; for i in {1..30}; do if curl -sSf http://127.0.0.1:4173/en/cv/ >/dev/null 2>&1 && curl -sSf http://127.0.0.1:4173/de/cv/ >/dev/null 2>&1; then READY=1; break; fi; sleep 1; done; if [ \"$READY\" -ne 1 ]; then echo \"Preview server did not become ready in time\"; exit 1; fi; bun run generate:cv-pdf'",
"check:i18n": "bun scripts/check-i18n-consistency.mjs",
"prepare": "svelte-kit sync",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
Expand Down
84 changes: 84 additions & 0 deletions scripts/check-i18n-consistency.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import fs from "node:fs";
import path from "node:path";

const BASE = path.resolve("src/lib/content");
const locales = ["en", "de"];
const requiredFiles = [
"profile.md",
"contact.md",
"skills.md",
"experience.md",
"education.md",
"languages.md",
"projects.md",
"certificates.md"
];

/**
* Lists all Markdown files in a given locale directory.
* @param {string} locale - The locale directory.
* @returns {string[]} List of Markdown file names.
*/
function listMarkdownFiles(locale) {
const dir = path.join(BASE, locale);
if (!fs.existsSync(dir)) {
throw new Error(`Missing locale directory: ${dir}`);
}

return fs
.readdirSync(dir, { withFileTypes: true })
.filter(entry => entry.isFile() && entry.name.endsWith(".md"))
.map(entry => entry.name)
.sort();
}

/**
* Asserts that all required files exist in a given locale.
* @param {string} locale - The locale directory.
* @param {string[]} files - List of Markdown file names.
*/
function assertRequiredFilesExist(locale, files) {
const missing = requiredFiles.filter(file => !files.includes(file));
if (missing.length > 0) {
throw new Error(`Locale "${locale}" is missing required files: ${missing.join(", ")}`);
}
}

/**
* Main function to run the i18n consistency check.
*/
function main() {
const localeFiles = Object.fromEntries(locales.map(locale => [locale, listMarkdownFiles(locale)]));

for (const locale of locales) {
assertRequiredFilesExist(locale, localeFiles[locale]);
}

const [first, ...rest] = locales;
for (const locale of rest) {
const missingInLocale = localeFiles[first].filter(file => !localeFiles[locale].includes(file));
const extraInLocale = localeFiles[locale].filter(file => !localeFiles[first].includes(file));

if (missingInLocale.length > 0 || extraInLocale.length > 0) {
throw new Error(
[
`File mismatch between "${first}" and "${locale}":`,
missingInLocale.length ? `- Missing in ${locale}: ${missingInLocale.join(", ")}` : "",
extraInLocale.length ? `- Extra in ${locale}: ${extraInLocale.join(", ")}` : ""
]
.filter(Boolean)
.join("\n")
);
}
}

console.log("✅ i18n content consistency check passed");
}

try {
main();
} catch (error) {
console.error("❌ i18n consistency check failed");
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
40 changes: 27 additions & 13 deletions scripts/generate-cv-pdf.mjs
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
import { chromium } from "playwright";

const BASE_URL = "http://127.0.0.1:4173";
const CV_URL = `${BASE_URL}/cv/`;

const outputPath = "static/colin-moerbe-cv.pdf";
const targets = [
{ locale: "en", url: `${BASE_URL}/en/cv/`, outputPath: "static/colin-moerbe-cv-en.pdf" },
{ locale: "de", url: `${BASE_URL}/de/cv/`, outputPath: "static/colin-moerbe-cv-de.pdf" }
];

/**
* Generates the CV PDF from the /cv route and writes it to the stable output path.
* Generates one PDF from a CV route.
*
* @returns {Promise<void>} Resolves when PDF generation is complete.
* @param {{ locale: string; url: string; outputPath: string }} target
* @returns {Promise<void>}
*/
async function generatePdf() {
async function generatePdfForTarget(target) {
const browser = await chromium.launch({ headless: true });

try {
const page = await browser.newPage();
const response = await page.goto(CV_URL, { waitUntil: "networkidle" });
const response = await page.goto(target.url, { waitUntil: "networkidle" });

if (!response || !response.ok()) {
const status = response?.status() ?? "NO_RESPONSE";
throw new Error(`Failed to load CV page for PDF generation (status: ${status}).`);
throw new Error(
`Failed to load ${target.locale.toUpperCase()} CV page for PDF generation (status: ${status}).`
);
}

await page.emulateMedia({ media: "print" });

await page.pdf({
path: outputPath,
path: target.outputPath,
format: "A4",
printBackground: true,
displayHeaderFooter: false,
Expand All @@ -37,21 +42,30 @@ async function generatePdf() {
}
});

console.log(`✅ Generated ${outputPath}`);
console.log(`✅ Generated ${target.outputPath}`);
} finally {
await browser.close();
}
}

/**
* Handles PDF generation failures and exits the process with an error code.
* Generates all localized CV PDFs.
*
* @param {unknown} err - The thrown error.
* @returns {never} This function always terminates the process.
* @returns {Promise<void>}
*/
async function generateAllPdfs() {
await Promise.all(targets.map(target => generatePdfForTarget(target)));
}

/**
* Handles PDF generation failures and exits with an error.
*
* @param {unknown} err
* @returns {never}
*/
function handlePdfGenerationError(err) {
console.error("❌ PDF generation failed:", err);
process.exit(1);
}

generatePdf().catch(handlePdfGenerationError);
generateAllPdfs().catch(handlePdfGenerationError);
13 changes: 11 additions & 2 deletions src/lib/components/CertificateCard.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
<script lang="ts">
/**
* Certificate card component for displaying a certificate entry.
*
* @property title Certificate title.
* @property info Supporting text (issuer/date).
* @property link Optional URL to the certificate.
* @property ctaLabel Call-to-action label for the certificate link.
*/
interface Props {
title: string;
info: string;
link?: string;
ctaLabel: string;
}

const { title, info, link }: Props = $props();
const { title, info, link, ctaLabel }: Props = $props();
</script>

<div
Expand All @@ -21,7 +30,7 @@
target="_blank"
rel="noopener noreferrer"
class="text-sm font-semibold text-blue-600 hover:underline dark:text-blue-400">
View Certificate
{ctaLabel}
</a>
</div>
{/if}
Expand Down
7 changes: 7 additions & 0 deletions src/lib/content/de/certificates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Zertifikate

## Microsoft zertifiziert: Azure Grundlagen

_Microsoft, 2024_

- [Zum Zertifikat](https://learn.microsoft.com/de-de/users/colinmoerbe/credentials/3f640b77279204a5)
9 changes: 9 additions & 0 deletions src/lib/content/de/contact.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Kontakt

- **Name:** Colin Mörbe
- **Role:** Backend Software Engineer
- **Location:** 72654 Neckartenzlingen
- **Phone:** +49 1577 5842277
- **Email:** colin@familie-moerbe.de
- **LinkedIn:** [linkedin.com/in/colin-moerbe](https://linkedin.com/in/colin-moerbe)
- **GitHub:** [github.com/Colin23](https://github.com/Colin23)
13 changes: 13 additions & 0 deletions src/lib/content/de/education.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Ausbildung

## 03/2020 – 08/2023

### Hochschule Reutlingen

B.Sc. Medien- und Kommunikationsinformatik (Note: 2,0)

## 10/2018 – 02/2020

### Universität Stuttgart

B.Sc. Softwaretechnik
38 changes: 38 additions & 0 deletions src/lib/content/de/experience.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Berufserfahrung

## 09/2023 – Heute

### SSC-Services GmbH — Böblingen

**Software Engineer**

- Entwicklung und kontinuierliche Verbesserung einer containerisierten Microservice-Landschaft (~15–20 Services) in
einer unternehmensweiten Systemumgebung.
- Analyse, Design und technische Umsetzung von Geschäftsanforderungen in enger Zusammenarbeit mit dem Kunden.
- Entwicklung von ereignis- und workflowbasierten Backend-Services für die Prozessautomatisierung.
- Implementierung von hochleistungsfähigen Delta-Load-Migrationsprozessen (mehr als drei Millionen Datensätze) unter
Verwendung von Parallelisierung.
- Entwicklung von Kafka-basierten Integrationsschnittstellen mit hohem Durchsatz.
- Entwicklung, Containerisierung und Betrieb von Backend-Services auf Kubernetes (Azure) sowie Aufbau einer
AWS-Infrastruktur mit CDK (Java).
- Verbesserung der CI/CD-Pipelines und nachhaltige Reduzierung wiederkehrender Produktionsprobleme durch
Refaktorisierung und verbesserte Protokollierungs- und Überwachungsstrategien.
- Leitung einer unternehmensweiten Lerninitiative, einschließlich Design und Entwicklung eines internen Tools zur
Abbildung von Lernpfaden und Kompetenzen.

## 11/2022 – 06/2023

### envite consulting GmbH — Stuttgart

**Werkstudent DevOps Engineer**

- Implementierung von CI/CD-Pipelines.
- Entwicklung einer Methodik zur Schätzung des Energieverbrauchs von Build- und Deployment-Pipelines.

## 03/2022 – 08/2022

### pronexon GmbH — Reutlingen

**Praktikum, System Integration**

- Entwicklung und Implementierung von Server-, Speicher- und Backup-Lösungen.
4 changes: 4 additions & 0 deletions src/lib/content/de/languages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Sprachen

- Deutsch (Muttersprache)
- Englisch (verhandlungssicher)
5 changes: 5 additions & 0 deletions src/lib/content/de/profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Profil

Backend-Softwareentwickler mit Schwerpunkt auf modularen, ereignisbasierten Systemen in Cloud-Umgebungen. Erfahrung in
der Konzeption und Entwicklung skalierbarer Backend-Services sowie im Betrieb containerisierter Anwendungen auf
Kubernetes (Azure) mit einem starken Fokus auf Architektur, Automatisierung und Systemzuverlässigkeit.
12 changes: 12 additions & 0 deletions src/lib/content/de/projects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Projekte

## Persönliches Portfolio

_SvelteKit, Tailwind CSS, Bun, mdsvex_

- Eine hochperformante, SEO-freundliche Portfolio-Website.
- Automatisierte PDF-Generierung aus Markdown-Inhalten.
- Statisches Hosting auf Netlify.

[GitHub](https://github.com/Colin23/portfolio)
[Live Demo](https://colinmoerbe.com)
Loading