Vue
Server-render Vue 3 components with Composition API and full TypeScript support.
#Build Configuration
Add Vue to your build by specifying the directory containing your Vue components:
// absolute.config.ts
import { defineConfig } from '@absolutejs/absolute';
export default defineConfig({
vueDirectory: 'frontend/vue'
});#Async Resources
useResource() provides component-scoped async state with loading and error refs, abort-on-unmount, refresh, cancellation, and optimistic mutation. Choose a shared cache such as TanStack Query or @absolutejs/sync when data must survive navigation or be shared across components.
<script setup lang="ts">
import { useResource } from '@absolutejs/absolute/vue';
const profile = useResource((signal) =>
fetch('/api/profile', { signal }).then((response) => response.json())
);
const rename = async (name: string) => {
const updated = await saveProfile({ name });
profile.mutate(updated);
};
</script>
<template>
<Spinner v-if="profile.loading.value" />
<ErrorMessage v-else-if="profile.error.value" />
<ProfileCard v-else :profile="profile.data.value" @rename="rename" />
<button @click="profile.refresh()">Refresh</button>
</template>#Page Handler
Use handleVuePageRequest with asset() to get the compiled paths for both the page and index files. The fourth parameter is a head tag string generated by generateHeadElement:
Framework handlers are non-streaming by default. Add { collectStreamingSlots: true } as the final argument to opt into out-of-order slot streaming for framework primitives.
// backend/server.ts
import { asset, generateHeadElement } from '@absolutejs/absolute';
import { handleVuePageRequest } from '@absolutejs/absolute/vue';
new Elysia()
.get('/products/:id', async ({ params }) => {
const product = await getProduct(params.id);
const relatedProducts = await getRelatedProducts(product.categoryId);
return handleVuePageRequest({
headTag: generateHeadElement({
title: `${product.name} | My Store`,
meta: [
{ name: 'description', content: product.description }
]
}),
indexPath: asset(manifest, 'ProductsIndex'),
pagePath: asset(manifest, 'Products'),
props: { product, relatedProducts }
});
})#Generate Head
generateHeadElement creates the head tag string from a structured object. This provides a type-safe way to set titles, meta tags, and link elements:
import { generateHeadElement } from '@absolutejs/absolute';
// generateHeadElement creates the head tag string for you
const head = generateHeadElement({
title: 'Page Title',
meta: [
{ name: 'description', content: 'Page description' },
{ name: 'keywords', content: 'vue, ssr, absolutejs' },
{ property: 'og:title', content: 'Open Graph Title' },
{ property: 'og:image', content: '/images/og.png' }
],
link: [
{ rel: 'canonical', href: 'https://example.com/page' },
{ rel: 'icon', href: '/favicon.ico' }
]
});
// Returns a string like:
// <title>Page Title</title>
// <meta name="description" content="Page description">
// <meta property="og:title" content="Open Graph Title">
// <link rel="canonical" href="https://example.com/page">- title: Sets the page title
- meta: Array of meta tags with name/property and content
- link: Array of link elements for canonical URLs, icons, etc.
#Components
Use Vue 3 Single File Components with the Composition API. defineProps provides type-safe prop access:
<!-- src/vue/pages/Products.vue -->
<script setup lang="ts">
type Product = {
id: string;
name: string;
price: number;
description: string;
};
type ProductsProps = {
product: Product;
relatedProducts: Product[];
};
const props = defineProps<ProductsProps>();
</script>
<template>
<div class="product-page">
<h1>{{ props.product.name }}</h1>
<p class="price">${{ props.product.price }}</p>
<p>{{ props.product.description }}</p>
<section v-if="props.relatedProducts.length > 0">
<h2>Related Products</h2>
<ul>
<li v-for="related in props.relatedProducts" :key="related.id">
<a :href="`/products/${related.id}`">{{ related.name }}</a>
</li>
</ul>
</section>
</div>
</template>
<style scoped>
.price {
font-size: 1.5rem;
color: var(--primary);
}
</style>#Vue Imports
When using Vue and Svelte in the same project, TypeScript may have conflicts between .vue and .svelte file type definitions. Import all Vue components in a separate file and export that object to avoid these conflicts.
import { asset, generateHeadElement } from '@absolutejs/absolute';
import { handleSveltePageRequest } from '@absolutejs/absolute/svelte';
import { handleVuePageRequest } from '@absolutejs/absolute/vue';
import { vueImports } from './vueImporter';
export const server = new Elysia()
.get('/svelte', async () =>
handleSveltePageRequest({
indexPath: asset(manifest, 'SvelteExampleIndex'),
pagePath: asset(manifest, 'SvelteExample'),
props: {
cssPath: asset(manifest, 'SvelteExampleCSS'),
initialCount: 0
}
})
)
.get('/vue', () =>
handleVuePageRequest({
Page: vueImports.VueExample,
headTag: generateHeadElement({
cssPath: asset(manifest, 'VueExampleCSS'),
title: 'AbsoluteJS + Vue'
}),
indexPath: asset(manifest, 'VueExampleIndex'),
pagePath: asset(manifest, 'VueExample'),
props: { initialCount: 0 }
})
)
;This is only required when both Vue and Svelte are used in the same project. If you're only using Vue, you can omit this option.