diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..e3ccc8e
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,66 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Development Commands
+
+**Start development server:**
+
+```bash
+npm run dev
+```
+
+**Build for production:**
+
+```bash
+npm run build
+```
+
+**Type checking and linting:**
+
+```bash
+npm run check # Type check with svelte-check
+npm run lint # Check formatting and linting
+npm run format # Auto-format code with prettier
+```
+
+**Preview production build:**
+
+```bash
+npm run preview
+```
+
+## Architecture Overview
+
+This is a SvelteKit personal portfolio site for @jedmund that integrates with multiple external APIs to display real-time data about music listening habits and gaming activity.
+
+### Key Architecture Components
+
+**API Integration Layer** (`src/routes/api/`)
+
+- **Redis caching**: Shared Redis client (`redis-client.ts`) used across API routes for caching external API responses
+- **External APIs**: Last.fm (music), Steam (games), PSN (PlayStation games), Giant Bomb (game metadata)
+- **Data enrichment**: Last.fm data is enhanced with iTunes artwork and Giant Bomb metadata
+
+**Frontend Structure**
+
+- **Component-based**: Reusable Svelte components in `$lib/components/`
+- **Page composition**: Main page (`+page.svelte`) composed of multiple `Page` components with different content sections
+- **Data loading**: Server-side data fetching in `+page.ts` with error handling
+
+**Styling System**
+
+- **SCSS-based**: Global variables, fonts, themes automatically imported via Vite config
+- **Asset management**: SVG icons and illustrations with automatic processing and alias imports
+
+### Key Aliases (svelte.config.js)
+
+- `$components` → `src/lib/components`
+- `$icons` → `src/assets/icons`
+- `$illos` → `src/assets/illos`
+- `$styles` → `src/styles`
+
+### Environment Dependencies
+
+- Requires `LASTFM_API_KEY` and `REDIS_URL` environment variables
+- Uses Node.js adapter for deployment
diff --git a/eslint.config.js b/eslint.config.js
index a351fa9..5a7eb2c 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,8 +1,8 @@
-import js from '@eslint/js';
-import ts from 'typescript-eslint';
-import svelte from 'eslint-plugin-svelte';
-import prettier from 'eslint-config-prettier';
-import globals from 'globals';
+import js from '@eslint/js'
+import ts from 'typescript-eslint'
+import svelte from 'eslint-plugin-svelte'
+import prettier from 'eslint-config-prettier'
+import globals from 'globals'
/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
@@ -30,4 +30,4 @@ export default [
{
ignores: ['build/', '.svelte-kit/', 'dist/']
}
-];
+]
diff --git a/package-lock.json b/package-lock.json
index 59bd17a..0cb1e0a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,7 +13,9 @@
"@types/steamapi": "^2.2.5",
"dotenv": "^16.4.5",
"giantbombing-api": "^1.0.4",
+ "gray-matter": "^4.0.3",
"ioredis": "^5.4.1",
+ "marked": "^15.0.12",
"node-itunes-search": "^1.2.3",
"psn-api": "github:jedmund/psn-api",
"redis": "^4.7.0",
@@ -2471,6 +2473,19 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/esquery": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
@@ -2533,6 +2548,18 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
+ "node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
@@ -2881,6 +2908,43 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
+ "node_modules/gray-matter": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
+ "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-yaml": "^3.13.1",
+ "kind-of": "^6.0.2",
+ "section-matter": "^1.0.0",
+ "strip-bom-string": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/gray-matter/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/gray-matter/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
@@ -3069,6 +3133,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3261,6 +3334,15 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -3348,6 +3430,18 @@
"@jridgewell/sourcemap-codec": "^1.4.15"
}
},
+ "node_modules/marked": {
+ "version": "15.0.12",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
+ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
@@ -4196,6 +4290,19 @@
"node": ">=14.0.0"
}
},
+ "node_modules/section-matter": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
+ "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
+ "license": "MIT",
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/semver": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
@@ -4288,6 +4395,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/sshpk": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
@@ -4421,6 +4534,15 @@
"node": ">=8"
}
},
+ "node_modules/strip-bom-string": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
+ "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
diff --git a/package.json b/package.json
index 192950c..1757ed8 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,9 @@
"@types/steamapi": "^2.2.5",
"dotenv": "^16.4.5",
"giantbombing-api": "^1.0.4",
+ "gray-matter": "^4.0.3",
"ioredis": "^5.4.1",
+ "marked": "^15.0.12",
"node-itunes-search": "^1.2.3",
"psn-api": "github:jedmund/psn-api",
"redis": "^4.7.0",
diff --git a/src/app.d.ts b/src/app.d.ts
index 743f07b..f70d0e1 100644
--- a/src/app.d.ts
+++ b/src/app.d.ts
@@ -10,4 +10,4 @@ declare global {
}
}
-export {};
+export {}
diff --git a/src/assets/icons/labs.svg b/src/assets/icons/labs.svg
new file mode 100644
index 0000000..83b42fb
--- /dev/null
+++ b/src/assets/icons/labs.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/universe.svg b/src/assets/icons/universe.svg
new file mode 100644
index 0000000..66bff28
--- /dev/null
+++ b/src/assets/icons/universe.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/work.svg b/src/assets/icons/work.svg
new file mode 100644
index 0000000..ce09cba
--- /dev/null
+++ b/src/assets/icons/work.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/styles/variables.scss b/src/assets/styles/variables.scss
index 5c5f680..1dd4be8 100644
--- a/src/assets/styles/variables.scss
+++ b/src/assets/styles/variables.scss
@@ -25,6 +25,7 @@ $unit-20x: $unit * 20;
/* Page properties
* -------------------------------------------------------------------------- */
$page-corner-radius: $unit;
+$card-corner-radius: $unit-3x;
$page-top-margin: $unit-6x;
@@ -86,6 +87,20 @@ $grey-color: #f0f0f0;
$image-border-color: rgba(0, 0, 0, 0.03);
+/* Shadows
+ * -------------------------------------------------------------------------- */
+$card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+$card-shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.12);
+
+/* Pill colors
+ * -------------------------------------------------------------------------- */
+$work-bg: #ffcdc5;
+$work-color: #d0290d;
+$universe-bg: #ffebc5;
+$universe-color: #b97d14;
+$labs-bg: #c5eaff;
+$labs-color: #1482c1;
+
$facebook-color: #3b5998;
$twitter-color: #55acee;
$instagram-color: #3f729b;
diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte
new file mode 100644
index 0000000..290da4b
--- /dev/null
+++ b/src/lib/components/Header.svelte
@@ -0,0 +1,52 @@
+
+
+ {post.excerpt}{post.title}
+ {/if}
+
+ {post.title}
+ {/if}
+
No posts found.
+ {/if} +