fixes for svelte 5 and hetzner deployment

This commit is contained in:
saiminh 2026-01-07 20:41:01 +13:00
parent 2b48498f72
commit d7c2ad8ea0
11 changed files with 704 additions and 275 deletions

2
.npmrc Normal file
View file

@ -0,0 +1,2 @@
legacy-peer-deps=true

798
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,7 @@
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"start": "node build",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
@ -22,7 +23,7 @@
"@typescript-eslint/parser": "^5.45.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.30.0",
"eslint-plugin-svelte": "^3.0.0",
"prettier": "^3.0.0",
"prettier-plugin-svelte": "^3.0.0",
"sass": "^1.64.2",

View file

@ -1,41 +1,58 @@
type Post = {
metadata: {
title: string
date: string
date?: string
description: string
tags: string[]
header_bg_image: string
svg: string
video: string
tags?: string[]
header_bg_image?: string
svg?: string
video?: string
order: number
}
default: {
render: () => string
}
path: string
Content: string
default: unknown // Component, but we don't need to render it for the list page
}
export const fetchMarkdownPosts = async () => {
// eslint-disable-next-line no-useless-escape
const allPostFiles = import.meta.glob('/src/routes/work/md/\*.md')
try {
const allPostFiles = import.meta.glob('/src/routes/work/md/*.md', { eager: false })
const iterablePostFiles = Object.entries(allPostFiles)
const iterablePostFiles = Object.entries(allPostFiles)
const allPosts = await Promise.all(
iterablePostFiles.map(async ([path, resolver]) => {
const postPath = path.slice(11, -3).replace('work/md/','work/')
const data: unknown = await resolver()
const postData = data as Post
const content = postData.default.render() as unknown as { html: string }
if (iterablePostFiles.length === 0) {
console.warn('No markdown files found')
return []
}
return {
meta: postData.metadata,
path: postPath,
Content: content.html,
}
})
)
const allPosts = await Promise.all(
iterablePostFiles.map(async ([path, resolver]) => {
try {
// Path will be like '/src/routes/work/md/adidas.md'
// We want '/work/adidas'
const pathWithoutExt = path.slice(0, -3) // Remove '.md'
const pathParts = pathWithoutExt.split('/')
const slug = pathParts[pathParts.length - 1] // Get 'adidas'
const postPath = `/work/${slug}`
return allPosts
const data: unknown = await resolver()
const postData = data as Post
// Only extract metadata - we don't need to render the content for the list page
// The work page only uses metadata (title, svg, tags) and path
return {
meta: postData.metadata,
path: postPath,
}
} catch (error) {
console.error(`Error loading post from ${path}:`, error)
return null
}
})
)
// Filter out any null results from failed imports
return allPosts.filter((post): post is NonNullable<typeof post> => post !== null)
} catch (error) {
console.error('Error in fetchMarkdownPosts:', error)
return []
}
}

View file

@ -3,11 +3,21 @@
import Header from '$lib/components/Header.svelte';
import Loader from '$lib/components/Loader.svelte';
import { navigating, page } from '$app/stores';
import { onMount } from 'svelte';
import type { Snippet } from 'svelte';
interface LayoutData {
pathname: string;
}
let { data, children }: { data: LayoutData, children: Snippet } = $props();
// Track pathname changes - if pathname changes but navigating is still true,
// navigation might be stuck (though this shouldn't happen normally)
let lastPathname = $state(data.pathname);
$effect(() => {
if (data.pathname !== lastPathname) {
lastPathname = data.pathname;
}
});
</script>
<svelte:head>

View file

@ -142,8 +142,10 @@
bulgefilter.center = new PIXI.Point(mouse.x, 0.5);
})
let scrollTriggerTweens: Array<GSAPTween> = [];
function createScrollTrigger() {
gsap.to([textRows[0], textRows[1]], {
const tween1 = gsap.to([textRows[0], textRows[1]], {
y: '+=' + -textRows[0][0].height * 0.9,
duration: 1,
ease: 'none',
@ -155,7 +157,7 @@
// markers: true,
}
})
gsap.to([textRows[2], textRows[3]], {
const tween2 = gsap.to([textRows[2], textRows[3]], {
y: '+=' + textRows[0][0].height * 0.9,
duration: 1,
ease: 'none',
@ -167,11 +169,22 @@
// markers: true,
}
})
scrollTriggerTweens.push(tween1, tween2);
}
return () => {
app.destroy(true, { children: true, texture: true, baseTexture: true });
// IMPORTANT: Kill ScrollTrigger and GSAP animations FIRST,
// before destroying PixiJS objects they reference
scrollTriggerTweens.forEach( tween => {
if (tween.scrollTrigger) {
tween.scrollTrigger.kill();
}
tween.kill();
});
// Kill all other tweens
tweens.forEach( tween => tween.kill() );
// Now safe to destroy PixiJS app
app.destroy(true, { children: true, texture: true, baseTexture: true });
}
})

View file

@ -1,12 +1,28 @@
import { fetchMarkdownPosts } from '../../lib/importMarkdown'
export async function load() {
const posts = await fetchMarkdownPosts()
if (!posts) console.error('No posts found')
try {
const posts = await fetchMarkdownPosts()
if (!posts) {
console.error('No posts found')
return {
posts: [],
title: 'Work References',
description: 'A few of the projects I have worked on'
}
}
return {
posts,
title: 'Work References',
description: 'A few of the projects I have worked on'
return {
posts,
title: 'Work References',
description: 'A few of the projects I have worked on'
}
} catch (error) {
console.error('Error loading work posts:', error)
return {
posts: [],
title: 'Work References',
description: 'A few of the projects I have worked on'
}
}
}

View file

@ -4,9 +4,9 @@
import { SplitText } from 'gsap/SplitText';
let { data } = $props();
const orderedPosts = data.posts.sort((a, b) => {
const orderedPosts = $derived.by(() => (data.posts || []).sort((a, b) => {
return a.meta.order - b.meta.order;
});
}));
onMount(() => {
document.querySelectorAll('.workclone')?.forEach(clone => {

View file

@ -3,7 +3,8 @@ export async function load( { params }: { params: { slug: string }} ){
const post = await import(`../md/${params.slug}.md`)
const { title = '', date = '', header_bg_image = '', svg = '', video = '', tags = [], reference = '', referenceName = '', tasks = [], description = [], images = [], agency = '', agencyName = '' } = post.metadata
const Content = post.default.render()
// Don't pass the component - it's not serializable
// Import it directly in the page component instead
return {
title,
@ -12,7 +13,7 @@ export async function load( { params }: { params: { slug: string }} ){
svg,
video,
tags,
Content,
slug: params.slug,
reference,
referenceName,
tasks,
@ -23,5 +24,6 @@ export async function load( { params }: { params: { slug: string }} ){
}
} catch (error) {
console.error(error)
throw error
}
}

View file

@ -5,9 +5,18 @@
import { CldImage } from 'svelte-cloudinary';
let { data } = $props();
let visible = false;
let visible = $state(false);
let Content = $state<any>(null);
onMount(() => {
// Import the markdown component dynamically and set up animations
onMount(async () => {
// Load the markdown component
try {
const post = await import(`../md/${data.slug}.md`);
Content = post.default;
} catch (error) {
console.error('Error loading markdown component:', error);
}
gsap.registerPlugin(ScrollToPlugin);
@ -136,7 +145,7 @@
{:else}
<CldImage
src={image}
alt="{data.title}"
alt={data.title}
sizes="(min-width: 768px) 67vw, 90vw"
width={1400}
height={840}
@ -178,7 +187,9 @@
{/if}
</div>
<div class="work-content-text">
{@html data.Content.html}
{#if Content}
<svelte:component this={Content} />
{/if}
</div>
</div>
{/if}

View file

@ -1,6 +1,6 @@
// import adapter from '@sveltejs/adapter-auto';
// import adapter from '@sveltejs/adapter-node';
import adapter from '@sveltejs/adapter-netlify';
import adapter from '@sveltejs/adapter-node';
// import adapter from '@sveltejs/adapter-netlify';
import preprocess from 'svelte-preprocess';
import { mdsvex } from 'mdsvex'
@ -20,13 +20,12 @@ const config = {
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter({
// if true, will create a Netlify Edge Function rather
// than using standard Node-based functions
edge: false,
// if true, will split your app into multiple functions
// instead of creating a single one for the entire app.
// if `edge` is true, this option cannot be used
split: false
// Output directory for the built server
out: 'build',
// Precompress output files (gzip and brotli)
precompress: true,
// Enable polyfills for Node.js built-in modules
polyfills: true
}),
csp: {
mode: 'auto',