Over the last decade, I’ve been working on a lot of projects; this website was one of it. Logically, I tried a lot of tools and frameworks, on the one hand, to improve the current project as well as my workflow and my toolset, but also to develop myself further. Well, and to keep up with the times… After upgrading this website to the newest version 4, I thought it would be a good idea to share my current stack and experience.
A lot of websites are just informative, they don’t need a lot of functionality. Take, for example, this website, it simply shows some information about me, contains this (static) blog and presents information about made hiking routes. And only the hiking routes are a lit bit more complex, the rest is just static content with little interactivity like the mobile menu or the theme switcher.
So, I used Vue3 with Vite for the third version of this website. This was a large improvement over the previous version, which was built with Vue2 and Webpack. The backend remained the same, it’s still Spring Boot and mainly provides the image proxy, so my user doesn’t transfer data to other hosts.
I used Vue’s lazy loading feature so navigate between pages is extremely fast, only a few bytes are transferred. The major contributor to the size of this site is the design, the content is tiny in comparison. The lazy loading feature only loads the changed content, so the user doesn’t have to download the whole page again resulting in a much faster page load. Developing with Vue+TypeScript+SCSS is a pleasure, you can use the most recent features of frameworks and languages. However, there are some drawbacks:
//reads BlogIndex.vue and replaces let posts = []; with the blog entries array
const fileContent = fs.readFileSync(`${BLOG_BASEPATH}BlogIndex.vue`).toString();
const newFileContent = fileContent.replace(/let posts = \[\];/, `let posts = ${JSON.stringify(entries)};`)
fs.writeFileSync(BLOG_BASEPATH + "/_BlogIndex.vue", newFileContent)
Furthermore, the search engine optimization is bad because the Vue code is only executed at runtime in the user’s browser and search engines usually cannot execute Javascript or only a simple version of it. As a solution to this problem, there are different packages that usually spawn a browser and call the Vue devserver for each page. The content is then used as the prerendered value, which is more search engine friendly. However, this did not exist at the time of Vue3, and on the other hand, it is very error-prone.
Vue components are hierarchically nested. As a result, the layout is also nested, and it is therefore difficult to create different layouts for e.g. blog post and normal pages.
Creating a robots.txt or sitemap.xml dynamically requires third party tools and is not that easy.
Therefore, when I found the new framework Astro, I thought it could be worth a shot.
Astro is a framework which translates your md, mdx and JavaScript files to Native HTML. It is there for search engine optimized. However, you are able to use your favorite framework like Vue to write ones specific component You are writing the overall layout and specific UI components like the header or the footer in JSX and the content in md or mdx. Whenever you need a component which is not supported by Astro, you can write it in your favorite framework and import it.
So in my case, I translated all the tiny functionality of the Vue components to Astro components, for example, the theme switcher and the mobile menu. Before:
<template>
<!-- ... -->
<div class="navbar-menu is-dark" :class="{'is-active': burger}"></div>
<!-- ... -->
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" @click="showBurger()"></a>
<!-- ... -->
</template>
<script lang="ts" setup>
import {ref} from "vue";
const burger = ref(false)
const showBurger = () => {
burger.value = !burger.value
}
</script>
And after:
<script is:inline type="application/javascript">
const showBurger = () => {
document.getElementById("menu-burger").classList.toggle("is-active");
};
</script>
<!-- ... -->
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" onclick="showBurger()">
</a>
The hiking pages are still vue components, because they are more complex and I don’t want to rewrite them. However, it is very easy to import them into the Astro project:
---
import Layout from "../../layouts/Default.astro";
import Hiking from "./Hiking.vue";
---
<Layout title="Hiking" description="The overview page over my hiking routes">
<Hiking client:only></Hiking>
</Layout>
It is important to remove the vue router, because Astro now handles the routing. Also, global vue plugins need to be configured in the main astro file.
// https://astro.build/config
export default defineConfig({
markdown: {},
site: 'https://alindner.org',
integrations: [
mdx(),
sitemap(),
vue({
appEntrypoint: '/src/Vue.mts'
}),
image()
]
});
Which is this file:
import type {App} from "vue";
export default (app: App) => {
app.use()
}
At the time of writing, the fullscreen functionality of the hiking pages is not working stably, which as a little drawback of migrating a vue component to astro. If your vue components are too complex, you need to rewrite a lot of your components.
Astro allows you to safely write a blog.
---
import {getCollection} from "astro:content";
import Layout from "../../layouts/Default.astro";
const posts = (await getCollection("blog")).sort(
(a, b) => a.data.pubDate.valueOf() - b.data.pubDate.valueOf(),
);
---
<Layout title="Blog" description="Blog overview">
<div>
<div class="tile is-ancestor">
<div class="tile is-parent">
{posts.map((post) => (
<div class="tile is-vertical is-child box is-padded m-1">
<p class="title">
<a href={`/blog/${new Date(post.data.pubDate).toISOString().split('T')[0]}-${post.slug}/`}>
{post.data.title}
</a>
</p>
<p>
{post.data.description}
</p>
</div>
))}
</div>
</div>
</div>
</Layout>
The block of await getCollection("blog")
references the blog folder.
Astro automatically ensures type safety, so you can’t make a mistake when creating a new blog post.
All in all, it solves my challenges with the previous stack:
Some months ago I found PocketBase and I really like PocketBase:
You can use whatever frontend you want, I recommend using Angular. Angular’s service feature makes it very easy to access the API and use it in your angular components. To simplify it, I created a dedicated angular library for PocketBase: @ng-pocketbase/core. With this library you can crate a basic app with authentication in minutes.
Here I use Spring Boot in the backend and Angular in the frontend. Before, I also used Vue, but I found that Angular is more suitable for large applications. If your application grows with Angular, you can structure it in modules and components very useful while with Vue it gets a mess.
Spring Boot is a very powerful and flexible framework that is even very fast. It has a very large community and a lot of libraries, is easy to deploy and has a lot of documentation and tutorials. However, if you don’t like Java, use your favorite language and use a similar framework because mastering Java and Spring is hard and can take its time.