Generated
+12
@@ -7,6 +7,9 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "homepage",
|
"name": "homepage",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.24.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.3",
|
"@eslint/compat": "^1.2.3",
|
||||||
"@sveltejs/adapter-node": "^5.2.9",
|
"@sveltejs/adapter-node": "^5.2.9",
|
||||||
@@ -3583,6 +3586,15 @@
|
|||||||
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
|
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.24.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||||
|
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,5 +29,8 @@
|
|||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.0.0",
|
"typescript-eslint": "^8.0.0",
|
||||||
"vite": "^5.4.11"
|
"vite": "^5.4.11"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.24.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<script>
|
||||||
|
// Initialize the theme in blocking manner once
|
||||||
|
if (window) {
|
||||||
|
const theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
document.body.setAttribute('data-theme', theme);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { building, dev } from '$app/environment';
|
||||||
|
|
||||||
|
export const handle = async ({ event, resolve }) => {
|
||||||
|
if (dev || building) return resolve(event);
|
||||||
|
|
||||||
|
const auth = event.request.headers.get("Authorization");
|
||||||
|
|
||||||
|
if (auth !== `Basic ${btoa('efstajas:super-secret')}`) {
|
||||||
|
return new Response("Not authorized", {
|
||||||
|
status: 401,
|
||||||
|
headers: {
|
||||||
|
"WWW-Authenticate":
|
||||||
|
'Basic realm="User Visible Realm", charset="UTF-8"',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(event);
|
||||||
|
};
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from "$app/state";
|
||||||
|
import { quadInOut } from "svelte/easing";
|
||||||
|
import { Tween } from "svelte/motion";
|
||||||
|
|
||||||
|
const navTargets = [
|
||||||
|
{ name: "Writing", href: "/" },
|
||||||
|
{ name: "Projects", href: "/projects" },
|
||||||
|
{ name: "About & Contact", href: "/about" }
|
||||||
|
]
|
||||||
|
|
||||||
|
const navElems: HTMLAnchorElement[] = $state([]);
|
||||||
|
|
||||||
|
function calcSelectorOffsetAndWidth(routeId: string | null) {
|
||||||
|
if (routeId === null) return;
|
||||||
|
|
||||||
|
const targetIndex = navTargets.findIndex(({ href }) => href === routeId);
|
||||||
|
if (targetIndex === undefined) return;
|
||||||
|
|
||||||
|
const target = navElems[targetIndex];
|
||||||
|
|
||||||
|
return {
|
||||||
|
offset: target.offsetLeft - 16,
|
||||||
|
width: target.offsetWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectorOffset: undefined | Tween<number> = $state(undefined);
|
||||||
|
let selectorWidth: undefined | Tween<number> = $state(undefined);
|
||||||
|
$effect(() => {
|
||||||
|
const { offset, width } = calcSelectorOffsetAndWidth(page.route.id) || {};
|
||||||
|
if (offset === undefined || width === undefined) return;
|
||||||
|
|
||||||
|
if (selectorOffset) {
|
||||||
|
selectorOffset.set(offset);
|
||||||
|
} else {
|
||||||
|
selectorOffset = new Tween(offset, { duration: 300, easing: quadInOut });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectorWidth) {
|
||||||
|
selectorWidth.set(width);
|
||||||
|
} else {
|
||||||
|
selectorWidth = new Tween(width, { duration: 300, easing: quadInOut });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="name p"><strong>Jason Efstathiou</strong></h1>
|
||||||
|
<div class="opener">
|
||||||
|
<h1>I’m a freelance software developer and designer from Berlin, Germany.</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a id="nav" aria-label="navbar"></a>
|
||||||
|
<nav>
|
||||||
|
{#each navTargets as { name, href }, i}
|
||||||
|
<a bind:this={navElems[i]} {href}>{name}</a>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if selectorOffset && selectorWidth}
|
||||||
|
<div class="selector" style:transform="translateX({selectorOffset.current}px)" style:width="{selectorWidth.current}px"></div>
|
||||||
|
{/if}
|
||||||
|
<div class="line"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.name {
|
||||||
|
position: fixed;
|
||||||
|
top: 2rem;
|
||||||
|
left: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
width: calc(auto - 2rem);
|
||||||
|
margin: 0 0 0 calc(((100vw - min(calc(800px + 2rem), 100%)) / 2));
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
padding: 2rem 1rem 1.5rem 1rem;
|
||||||
|
background: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .line {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
height: 1px;
|
||||||
|
width: calc(100% - 1rem);
|
||||||
|
background: var(--color-foreground)
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 1rem;
|
||||||
|
width: 40px;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--color-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.opener {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
padding: 1rem;
|
||||||
|
padding-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { postMetadataSchema } from "$lib/utils/blog";
|
||||||
|
import { z } from "zod";
|
||||||
|
import PostIllustration from "./post-illustration.svelte";
|
||||||
|
|
||||||
|
let post: z.infer<typeof postMetadataSchema> & { slug: string } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a href={`/blog/${post.slug}#nav`}>
|
||||||
|
<PostIllustration {...post} />
|
||||||
|
<h1>{post.title}</h1>
|
||||||
|
<h2 class="p">{post.subtitle}</h2>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color-foreground);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { Tween } from "svelte/motion";
|
||||||
|
|
||||||
|
const {
|
||||||
|
illustrationPath,
|
||||||
|
illustrationForegroundColor,
|
||||||
|
illustrationBackgroundColor
|
||||||
|
}: {
|
||||||
|
illustrationPath: string;
|
||||||
|
illustrationForegroundColor: string;
|
||||||
|
illustrationBackgroundColor: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let path: SVGPathElement;
|
||||||
|
|
||||||
|
let strokeLength = $state(0);
|
||||||
|
let strokeOffset: Tween<number> | undefined = $state(undefined);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
strokeLength = path.getTotalLength();
|
||||||
|
strokeOffset = new Tween(strokeLength, { duration: 5000, });
|
||||||
|
strokeOffset.set(0);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style:width="100%" class="wrapper">
|
||||||
|
<svg viewBox="0 0 800 250" style:background={illustrationBackgroundColor}>
|
||||||
|
<path bind:this={path} d={illustrationPath} style:stroke-dasharray={strokeLength} style:stroke-dashoffset={strokeOffset?.current} fill="none" stroke-width="4" stroke={illustrationForegroundColor}/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
@@ -1 +0,0 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[data-theme="light"] {
|
||||||
|
--color-background: white;
|
||||||
|
--color-foreground: #202020;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--color-background: #202020;
|
||||||
|
--color-foreground: white;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
type Theme = 'light' | 'dark';
|
||||||
|
|
||||||
|
type ThemeStore = {
|
||||||
|
current: Theme;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getTheme() {
|
||||||
|
return (browser && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (browser) {
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||||
|
theme.current = getTheme();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const theme = $state<ThemeStore>({ current: getTheme() });
|
||||||
|
|
||||||
|
$effect.root(() => {
|
||||||
|
$effect(() => {
|
||||||
|
document.body?.setAttribute('data-theme', theme.current);
|
||||||
|
console.log('theme changed', theme.current);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Asserts that `condition` is truthy.
|
||||||
|
* @param condition The condition to assert.
|
||||||
|
* @param message Error message to throw on assertion error.
|
||||||
|
* @throw An error if `condition` is falsy.
|
||||||
|
*/
|
||||||
|
export default function assert(condition: unknown, message = 'Assertion Error'): asserts condition {
|
||||||
|
if (!condition) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const postMetadataSchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
subtitle: z.string(),
|
||||||
|
date: z.string(),
|
||||||
|
illustrationPath: z.string(),
|
||||||
|
illustrationForegroundColor: z.string(),
|
||||||
|
illustrationBackgroundColor: z.string(),
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: 'Seven Years of Home Assistant'
|
||||||
|
subtitle: 'foo bar'
|
||||||
|
date: '2024-09-30'
|
||||||
|
illustrationPath: 'M817 26H544.5M24 446.5V165M420.5 26H544.5M420.5 26V63.25L420.5 85M420.5 26H39.5M24 165H147.5M24 165V41.5M544.5 26L672.54 154.04M420.5 85C411.94 85 405 91.9396 405 100.5C405 104.78 406.735 108.655 409.54 111.46M420.5 85C429.06 85 436 91.9396 436 100.5C436 104.78 434.265 108.655 431.46 111.46M409.54 111.46C412.345 114.265 416.22 116 420.5 116C424.78 116 428.655 114.265 431.46 111.46M409.54 111.46L367.803 153.197M431.46 111.46L485 165H564.5M341 165C341 173.837 348.163 181 357 181C361.164 181 364.956 179.409 367.803 176.803M341 165C341 156.163 348.163 149 357 149C361.164 149 364.956 150.591 367.803 153.197M341 165H178.5M367.803 153.197C370.997 156.123 373 160.327 373 165C373 169.673 370.997 173.878 367.803 176.803M367.803 176.803L460.5 269.5L484.882 293.882M178.5 165C178.5 156.44 171.56 149.5 163 149.5C158.72 149.5 154.845 151.235 152.04 154.04M178.5 165C178.5 169.759 176.355 174.018 172.979 176.861M147.5 165C147.5 173.56 154.44 180.5 163 180.5C166.801 180.5 170.282 179.132 172.979 176.861M147.5 165C147.5 160.72 149.235 156.845 152.04 154.04M152.04 154.04L34.9602 36.9602M39.5 26C39.5 17.4396 32.5604 10.5 24 10.5C15.4396 10.5 8.5 17.4396 8.5 26C8.5 34.5604 15.4396 41.5 24 41.5M39.5 26C39.5 30.2802 37.7651 34.1552 34.9602 36.9602M24 41.5C28.2802 41.5 32.1552 39.7651 34.9602 36.9602M564.5 165C564.5 173.56 571.44 180.5 580 180.5C588.56 180.5 595.5 173.56 595.5 165C595.5 156.44 588.56 149.5 580 149.5C571.44 149.5 564.5 156.44 564.5 165ZM672.54 154.04C669.735 156.845 668 160.72 668 165C668 173.56 674.94 180.5 683.5 180.5C692.06 180.5 699 173.56 699 165C699 156.44 692.06 149.5 683.5 149.5C679.22 149.5 675.345 151.235 672.54 154.04ZM817 293.882H715.441M484.882 293.882H305.5M484.882 293.882H598.5M715.441 293.882L839.059 417.5M715.441 293.882H629.5M305.5 293.882C305.5 302.443 298.56 309.382 290 309.382M305.5 293.882C305.5 285.322 298.56 278.382 290 278.382C285.72 278.382 281.845 280.117 279.04 282.922M290 309.382C281.44 309.382 274.5 302.443 274.5 293.882C274.5 289.602 276.235 285.727 279.04 282.922M290 309.382V426M279.04 282.922L172.979 176.861M629.5 293.882C629.5 302.443 622.56 309.382 614 309.382C605.44 309.382 598.5 302.443 598.5 293.882M629.5 293.882C629.5 285.322 622.56 278.382 614 278.382C609.72 278.382 605.845 280.117 603.04 282.922C603.02 282.942 603.001 282.962 602.981 282.981C600.211 285.781 598.5 289.632 598.5 293.882'
|
||||||
|
illustrationForegroundColor: '#003A55'
|
||||||
|
illustrationBackgroundColor: '#87D9FF'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Hello
|
||||||
|
|
||||||
|
World!
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: 'Building a miniature data center at home'
|
||||||
|
subtitle: 'Documenting my homelab journey — from running PiHole to building a distributed, highly available application cluster'
|
||||||
|
date: '2025-09-30'
|
||||||
|
illustrationPath: 'M512 125H-24M651 127C651 202.663 589.663 264 514 264C438.337 264 377 202.663 377 127C377 51.337 438.337 -10 514 -10C589.663 -10 651 51.337 651 127ZM798 125.5C798 283.177 670.177 411 512.5 411C354.823 411 227 283.177 227 125.5C227 -32.1773 354.823 -160 512.5 -160C670.177 -160 798 -32.1773 798 125.5ZM602 126.5C602 175.377 562.377 215 513.5 215C464.623 215 425 175.377 425 126.5C425 77.6228 464.623 38 513.5 38C562.377 38 602 77.6228 602 126.5ZM554 126C554 148.644 535.644 167 513 167C490.356 167 472 148.644 472 126C472 103.356 490.356 85 513 85C535.644 85 554 103.356 554 126Z'
|
||||||
|
illustrationForegroundColor: '#773F00'
|
||||||
|
illustrationBackgroundColor: '#FFC787'
|
||||||
|
---
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque2 sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris.
|
||||||
|
|
||||||
|
Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet.
|
||||||
|
|
||||||
|
Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Header from '$lib/components/header.svelte';
|
||||||
|
import '$lib/state/theme/theme.svelte';
|
||||||
|
import '$lib/state/theme/theme.css';
|
||||||
|
import '../styles/global.css';
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<script>
|
||||||
|
window.__theme = {
|
||||||
|
setTheme: (theme) => {
|
||||||
|
document.body.style.cssText = theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<Header />
|
||||||
|
<main>
|
||||||
|
{@render children()}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body) {
|
||||||
|
background: var(--color-background);
|
||||||
|
color: var(--color-foreground);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import assert from '$lib/utils/assert.js';
|
||||||
|
import { postMetadataSchema } from '$lib/utils/blog';
|
||||||
|
|
||||||
|
export const load = async () => {
|
||||||
|
const posts = await Promise.all(
|
||||||
|
Object.entries(import.meta.glob('/src/posts/*.md')).map(async ([path, resolver]) => {
|
||||||
|
const resolved = await resolver();
|
||||||
|
|
||||||
|
assert(typeof resolved === 'object' && resolved && 'metadata' in resolved);
|
||||||
|
|
||||||
|
const metadata = postMetadataSchema.parse(resolved.metadata);
|
||||||
|
|
||||||
|
const slug = path.split('/').pop()?.slice(0, -3);
|
||||||
|
|
||||||
|
assert(slug);
|
||||||
|
|
||||||
|
return { ...metadata, slug };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedPosts = posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||||
|
|
||||||
|
return {
|
||||||
|
posts: sortedPosts,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prerender = true;
|
||||||
+23
-2
@@ -1,2 +1,23 @@
|
|||||||
<h1>Welcome to SvelteKit</h1>
|
<script lang="ts">
|
||||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
import PostCard from '$lib/components/post-card.svelte';
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="posts">
|
||||||
|
{#each data.posts as post}
|
||||||
|
<PostCard {...post} />
|
||||||
|
<PostCard {...post} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.posts {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4rem;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
about page
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import PostCard from '$lib/components/post-card.svelte';
|
||||||
|
|
||||||
|
const { data } = $props();
|
||||||
|
const { Post, metadata, slug } = data;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<PostCard {...metadata} {slug} />
|
||||||
|
<Post />
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
article {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { postMetadataSchema } from '$lib/utils/blog.js';
|
||||||
|
|
||||||
|
export const load = async ({ params }) => {
|
||||||
|
const { slug } = params;
|
||||||
|
|
||||||
|
const { default: Post, metadata: rawMetadata } = await import(`../../../posts/${slug}.md`);
|
||||||
|
const metadata = postMetadataSchema.parse(rawMetadata);
|
||||||
|
|
||||||
|
return {
|
||||||
|
Post,
|
||||||
|
metadata,
|
||||||
|
slug,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prerender = true;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
projects page
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Suisse Intl", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, .h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
p, .p {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, p {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
+4
-2
@@ -6,7 +6,9 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|||||||
const config = {
|
const config = {
|
||||||
// Consult https://svelte.dev/docs/kit/integrations
|
// Consult https://svelte.dev/docs/kit/integrations
|
||||||
// for more information about preprocessors
|
// for more information about preprocessors
|
||||||
preprocess: [vitePreprocess(), mdsvex()],
|
preprocess: [vitePreprocess(), mdsvex({
|
||||||
|
extensions: ['.md'],
|
||||||
|
})],
|
||||||
|
|
||||||
kit: {
|
kit: {
|
||||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||||
@@ -15,7 +17,7 @@ const config = {
|
|||||||
adapter: adapter()
|
adapter: adapter()
|
||||||
},
|
},
|
||||||
|
|
||||||
extensions: ['.svelte', '.svx']
|
extensions: ['.svelte', '.md']
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
Reference in New Issue
Block a user