Migrate to latest versions of Astro, Svelte and Tailwind

Fix hover in prose layout
This commit is contained in:
Mikkel Svartveit 2025-11-28 09:48:01 +01:00
parent 53ce4bd8ee
commit 332e1dea33
16 changed files with 3655 additions and 3704 deletions

View file

@ -1,13 +1,13 @@
import { defineConfig } from "astro/config"; import { defineConfig } from "astro/config";
import tailwind from "@astrojs/tailwind";
import svelte from "@astrojs/svelte"; import svelte from "@astrojs/svelte";
import mdx from "@astrojs/mdx"; import mdx from "@astrojs/mdx";
import cloudflare from "@astrojs/cloudflare"; import cloudflare from "@astrojs/cloudflare";
import tailwindcss from "@tailwindcss/vite";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
integrations: [tailwind(), svelte(), mdx()], integrations: [svelte(), mdx()],
output: "hybrid", output: "static",
adapter: cloudflare({ adapter: cloudflare({
imageService: "compile", imageService: "compile",
platformProxy: { platformProxy: {
@ -15,12 +15,10 @@ export default defineConfig({
}, },
}), }),
vite: { vite: {
plugins: [tailwindcss()],
ssr: { ssr: {
external: ["node:async_hooks"], external: ["node:async_hooks"],
}, },
}, },
experimental: {
actions: true,
},
site: "https://mikkelsvartveit.com", site: "https://mikkelsvartveit.com",
}); });

View file

@ -10,20 +10,22 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "^10.4.2", "@astrojs/check": "^0.9.6",
"@astrojs/mdx": "^3.1.3", "@astrojs/cloudflare": "^12.6.12",
"@astrojs/svelte": "^5.7.0", "@astrojs/mdx": "^4.3.12",
"@astrojs/tailwind": "^5.1.0", "@astrojs/svelte": "^7.2.2",
"astro": "^4.12.2", "astro": "^5.16.2",
"sharp": "^0.32.6", "sharp": "^0.34.5",
"svelte": "^4.2.18", "svelte": "^5.45.2",
"tailwindcss": "^3.4.7" "tailwindcss": "^4.1.17",
"typescript": "^5.9.3"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.19",
"prettier": "3.0.3", "@tailwindcss/vite": "^4.1.17",
"prettier-plugin-astro": "^0.12.3", "prettier": "^3.7.1",
"prettier-plugin-svelte": "^3.2.6", "prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.5.14" "prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.7.1"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,12 @@
import { defineAction, z, getApiContext } from "astro:actions"; import { defineAction } from "astro:actions";
import { z } from "astro:schema";
export const server = { export const server = {
registerView: defineAction({ registerView: defineAction({
input: z.object({ articleSlug: z.string() }), input: z.object({ articleSlug: z.string() }),
handler: async ({ articleSlug }) => { handler: async ({ articleSlug }, context) => {
const context = getApiContext();
// @ts-ignore // @ts-ignore
const { ViewCountKV } = context.locals.runtime.env; const ViewCountKV = context.locals.runtime?.env?.ViewCountKV;
let viewCount = (await ViewCountKV.get(articleSlug)) || 0; let viewCount = (await ViewCountKV.get(articleSlug)) || 0;
await ViewCountKV.put(articleSlug, ++viewCount); await ViewCountKV.put(articleSlug, ++viewCount);

View file

@ -4,14 +4,16 @@
import { expoIn } from "svelte/easing"; import { expoIn } from "svelte/easing";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
export let articleSlug: string; let { articleSlug }: { articleSlug: string } = $props();
let counter: number | null = null; let counter = $state<number | null>(null);
onMount(async () => { onMount(async () => {
const readCount = await actions.registerView({ articleSlug }); const { data, error } = await actions.registerView({ articleSlug });
counter = readCount; if (!error && data !== undefined) {
counter = data;
}
}); });
</script> </script>

View file

@ -1,5 +1,5 @@
--- ---
import { ViewTransitions } from "astro:transitions"; import { ClientRouter } from "astro:transitions";
interface Props { interface Props {
title?: string; title?: string;
@ -50,4 +50,4 @@ const fullImageUrl = origin + (image ?? "/favicon.png");
src="https://umami.mikkel.cloud/script.js" src="https://umami.mikkel.cloud/script.js"
data-website-id="1caff5b4-223d-47c3-8c5e-a5002d73b993"></script> data-website-id="1caff5b4-223d-47c3-8c5e-a5002d73b993"></script>
<ViewTransitions fallback="none" /> <ClientRouter fallback="none" />

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { slide, fade } from "svelte/transition"; import { slide, fade } from "svelte/transition";
export let currentPath: string; let { currentPath }: { currentPath: string } = $props();
$: currentPathTrimmed = currentPath.replace(/\/+$/, ""); const currentPathTrimmed = $derived(currentPath.replace(/\/+$/, ""));
const navbarContent = [ const navbarContent = [
{ name: "📝 Articles", href: "/articles" }, { name: "📝 Articles", href: "/articles" },
@ -10,7 +10,7 @@
{ name: "📷 Photography", href: "/photography" }, { name: "📷 Photography", href: "/photography" },
]; ];
let collapsed = true; let collapsed = $state(true);
</script> </script>
<header> <header>
@ -44,7 +44,7 @@
<button <button
aria-label="Open menu" aria-label="Open menu"
class="md:hidden" class="md:hidden"
on:click={() => (collapsed = !collapsed)} onclick={() => (collapsed = !collapsed)}
> >
<svg <svg
class="h-10 w-10 stroke-yellow-500" class="h-10 w-10 stroke-yellow-500"
@ -70,20 +70,20 @@
<div <div
role="presentation" role="presentation"
transition:fade={{ duration: 100 }} transition:fade={{ duration: 100 }}
class="fixed left-0 top-0 z-20 h-full w-full bg-black opacity-50" class="fixed top-0 left-0 z-20 h-full w-full bg-black opacity-50"
on:click={() => (collapsed = true)} onclick={() => (collapsed = true)}
on:keydown={(event) => { onkeydown={(event) => {
if (event.key === "Escape") collapsed = true; if (event.key === "Escape") collapsed = true;
}} }}
/> ></div>
<ul <ul
transition:slide={{ duration: 300, axis: "x" }} transition:slide={{ duration: 300, axis: "x" }}
class="fixed right-0 top-0 z-30 block h-full bg-white p-4" class="fixed top-0 right-0 z-30 block h-full bg-white p-4"
> >
<button <button
aria-label="Close menu" aria-label="Close menu"
on:click={() => (collapsed = true)} onclick={() => (collapsed = true)}
class="ml-auto block h-10 w-10" class="ml-auto block h-10 w-10"
> >
<svg <svg
@ -101,11 +101,11 @@
</button> </button>
{#each navbarContent as { name, href }} {#each navbarContent as { name, href }}
<li class="my-6 ml-2 mr-16"> <li class="my-6 mr-16 ml-2">
<a <a
{href} {href}
on:click={() => (collapsed = true)} onclick={() => (collapsed = true)}
class="whitespace-nowrap text-lg tracking-wide text-gray-600 decoration-yellow-400 decoration-2 underline-offset-8" class="text-lg tracking-wide whitespace-nowrap text-gray-600 decoration-yellow-400 decoration-2 underline-offset-8"
class:underline={href === currentPathTrimmed} class:underline={href === currentPathTrimmed}
> >
{name} {name}

View file

@ -1,6 +1,7 @@
--- ---
import Navbar from "@components/Navbar.svelte"; import Navbar from "@components/Navbar.svelte";
import HeadContent from "@components/HeadContent.astro"; import HeadContent from "@components/HeadContent.astro";
import "../styles/global.css";
--- ---
<html lang="en"> <html lang="en">
@ -11,10 +12,7 @@ import HeadContent from "@components/HeadContent.astro";
</head> </head>
<body class="overflow-y-scroll bg-gray-50"> <body class="overflow-y-scroll bg-gray-50">
<Navbar <Navbar client:load currentPath={Astro.url.pathname} />
client:load
currentPath={Astro.url.pathname}
/>
<main class="pb-6"> <main class="pb-6">
<slot /> <slot />

View file

@ -1,5 +1,5 @@
<div <div
class="prose-inline-code:rounded prose-inline-code:bg-slate-200 prose-inline-code:px-1 prose-inline-code:font-normal prose-inline-code:tracking-tight prose-inline-code:text-gray-700 before:prose-inline-code:content-[''] after:prose-inline-code:content-[''] prose prose-lg max-w-none font-serif prose-headings:text-gray-600 prose-h1:font-light prose-h1:leading-snug prose-h1:underline prose-h1:decoration-yellow-400 prose-h1:decoration-2 prose-h1:underline-offset-8 prose-p:text-gray-700 prose-a:text-emerald-700 prose-a:underline-offset-2 prose-a:duration-100 hover:prose-a:text-emerald-500 prose-strong:text-gray-700" class="prose-inline-code:rounded prose-inline-code:bg-slate-200 prose-inline-code:px-1 prose-inline-code:font-normal prose-inline-code:tracking-tight prose-inline-code:text-gray-700 before:prose-inline-code:content-[''] after:prose-inline-code:content-[''] prose prose-lg prose-headings:text-gray-600 prose-h1:font-light prose-h1:leading-snug prose-h1:underline prose-h1:decoration-yellow-400 prose-h1:decoration-2 prose-h1:underline-offset-8 prose-p:text-gray-700 prose-a:text-emerald-700 prose-a:underline-offset-2 prose-a:duration-100 prose-a:hover:text-emerald-500 prose-strong:text-gray-700 max-w-none font-serif"
> >
<slot /> <slot />
</div> </div>

View file

@ -5,7 +5,7 @@ import NotFoundImage from "../assets/images/404.jpeg";
--- ---
<BaseLayout> <BaseLayout>
<section class="mx-auto max-w-4xl px-3 pb-8 pt-12 sm:px-6"> <section class="mx-auto max-w-4xl px-3 pt-12 pb-8 sm:px-6">
<h1 <h1
class="mb-8 text-center font-serif text-3xl font-light tracking-wide text-gray-600 sm:text-4xl" class="mb-8 text-center font-serif text-3xl font-light tracking-wide text-gray-600 sm:text-4xl"
> >

View file

@ -2,23 +2,24 @@
import ContainerLayout from "@layouts/ContainerLayout.astro"; import ContainerLayout from "@layouts/ContainerLayout.astro";
import BaseLayout from "@layouts/BaseLayout.astro"; import BaseLayout from "@layouts/BaseLayout.astro";
import TextContentLayout from "@layouts/TextContentLayout.astro"; import TextContentLayout from "@layouts/TextContentLayout.astro";
import type { GetStaticPaths } from "astro"; import { getCollection, type CollectionEntry } from "astro:content";
import { getCollection } from "astro:content";
import ProseLayout from "@layouts/ProseLayout.astro"; import ProseLayout from "@layouts/ProseLayout.astro";
import ArticleViewCounter from "@components/ArticleViewCounter.svelte"; import ArticleViewCounter from "@components/ArticleViewCounter.svelte";
import HeadContent from "@components/HeadContent.astro"; import HeadContent from "@components/HeadContent.astro";
export const getStaticPaths = (async () => { export async function getStaticPaths() {
const blogCollection = await getCollection("blog"); const blogCollection = await getCollection("blog");
return blogCollection.map((entry) => ({ return blogCollection.map((entry) => ({
params: { params: { article: entry.slug },
article: entry.slug,
},
props: { entry }, props: { entry },
})); }));
}) satisfies GetStaticPaths; }
const project = Astro.props.entry; type Props = {
entry: CollectionEntry<"blog">;
};
const { entry: project } = Astro.props;
const { Content, headings } = await project.render(); const { Content, headings } = await project.render();
const { date, intro, image } = project.data; const { date, intro, image } = project.data;
const title = headings[0].text; const title = headings[0].text;

View file

@ -9,7 +9,7 @@ import portraitImage from "@assets/images/portrait.jpg";
const articles = await getCollection("blog"); const articles = await getCollection("blog");
const latestArticle = articles.sort( const latestArticle = articles.sort(
(p2, p1) => p1.data.date.getTime() - p2.data.date.getTime() (p2, p1) => p1.data.date.getTime() - p2.data.date.getTime(),
)[0]; )[0];
const latestArticleSlug = latestArticle.slug; const latestArticleSlug = latestArticle.slug;
const latestArticleTitle = (await latestArticle.render()).headings[0].text; const latestArticleTitle = (await latestArticle.render()).headings[0].text;
@ -18,9 +18,9 @@ const latestArticleImage = latestArticle.data.image;
<BaseLayout> <BaseLayout>
<div <div
class="mx-auto flex max-w-5xl flex-col-reverse items-stretch px-4 pb-6 pt-12 sm:px-6 md:flex-row" class="mx-auto flex max-w-5xl flex-col-reverse items-stretch px-4 pt-12 pb-6 sm:px-6 md:flex-row"
> >
<section class="mb-10 mt-4 w-full md:w-3/5"> <section class="mt-4 mb-10 w-full md:w-3/5">
<h1 <h1
class="mb-8 font-serif text-3xl font-light tracking-wide text-gray-600 sm:text-4xl" class="mb-8 font-serif text-3xl font-light tracking-wide text-gray-600 sm:text-4xl"
> >
@ -86,7 +86,7 @@ const latestArticleImage = latestArticle.data.image;
</a> </a>
</BaseLayout> </BaseLayout>
<footer class="mb-8 mt-20 flex w-full justify-center"> <footer class="mt-20 mb-8 flex w-full justify-center">
<p class="text-xs text-gray-500"> <p class="text-xs text-gray-500">
Built with Built with
<a <a

View file

@ -1,22 +1,29 @@
--- ---
import type { GetStaticPaths } from "astro";
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import type { ImageMetadata } from "astro";
import { getFileNameFromPath } from "./index.astro"; import { getFileNameFromPath } from "./index.astro";
import RootLayout from "@layouts/RootLayout.astro";
import HeadContent from "@components/HeadContent.astro"; import HeadContent from "@components/HeadContent.astro";
export const getStaticPaths = (async () => { interface PhotoModule {
const photos = await Astro.glob("../../assets/photos/*"); default: ImageMetadata;
}
return photos.map((photo: any) => ({ export async function getStaticPaths() {
const photoModules = import.meta.glob<PhotoModule>("../../assets/photos/*", {
eager: true,
});
const photos = Object.values(photoModules);
return photos.map((photo) => ({
params: { params: {
photo: getFileNameFromPath(photo.default.src), photo: getFileNameFromPath(photo.default.src),
}, },
props: { props: { photo },
photo,
},
})); }));
}) satisfies GetStaticPaths; }
interface Props {
photo: PhotoModule;
}
const { photo } = Astro.props; const { photo } = Astro.props;
--- ---

View file

@ -4,11 +4,19 @@ import ContainerLayout from "@layouts/ContainerLayout.astro";
import BaseLayout from "@layouts/BaseLayout.astro"; import BaseLayout from "@layouts/BaseLayout.astro";
import TextContentLayout from "@layouts/TextContentLayout.astro"; import TextContentLayout from "@layouts/TextContentLayout.astro";
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import type { ImageMetadata } from "astro";
import HeadContent from "@components/HeadContent.astro"; import HeadContent from "@components/HeadContent.astro";
const photos = await Astro.glob("../../assets/photos/*"); interface PhotoModule {
default: ImageMetadata;
}
const sortFiles = (a: any, b: any) => { const photoModules = import.meta.glob<PhotoModule>("../../assets/photos/*", {
eager: true,
});
const photos = Object.values(photoModules);
const sortFiles = (a: PhotoModule, b: PhotoModule) => {
const aNum = Number(a.default.src.match(/\d+/)); const aNum = Number(a.default.src.match(/\d+/));
const bNum = Number(b.default.src.match(/\d+/)); const bNum = Number(b.default.src.match(/\d+/));

View file

@ -3,7 +3,7 @@ import ContainerLayout from "@layouts/ContainerLayout.astro";
import BaseLayout from "@layouts/BaseLayout.astro"; import BaseLayout from "@layouts/BaseLayout.astro";
import TextContentLayout from "@layouts/TextContentLayout.astro"; import TextContentLayout from "@layouts/TextContentLayout.astro";
import type { GetStaticPaths } from "astro"; import type { GetStaticPaths } from "astro";
import { getCollection } from "astro:content"; import { getCollection, type CollectionEntry } from "astro:content";
import ProseLayout from "@layouts/ProseLayout.astro"; import ProseLayout from "@layouts/ProseLayout.astro";
import HeadContent from "@components/HeadContent.astro"; import HeadContent from "@components/HeadContent.astro";
@ -17,7 +17,11 @@ export const getStaticPaths = (async () => {
})); }));
}) satisfies GetStaticPaths; }) satisfies GetStaticPaths;
const project = Astro.props.entry; type Props = {
entry: CollectionEntry<"programming">;
};
const { entry: project } = Astro.props;
const { Content } = await project.render(); const { Content } = await project.render();
const { title, description, image, date, technologies, website, repository } = const { title, description, image, date, technologies, website, repository } =
project.data; project.data;

13
src/styles/global.css Normal file
View file

@ -0,0 +1,13 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@theme {
--font-sans: "Nunito", ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-serif: "Source Serif Pro", ui-serif, Georgia, Cambria,
"Times New Roman", Times, serif;
--font-mono: "Source Code Pro", ui-monospace, SFMono-Regular, Menlo, Monaco,
Consolas, "Liberation Mono", "Courier New", monospace;
}
@custom-variant prose-inline-code (&.prose :where(:not(pre)>code):not(:where([class~="not-prose"] *)));