AbsoluteJS

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:

TS
// 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.

VUE
<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.

TS
// 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:

TS
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:

VUE
<!-- 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.

TS
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.