Back to Dynamic Webapps
Vue.js Integration

Vue.js Integration Guide

Integrate SEO Sniper articles into your Vue 3 application using Composition API. Includes Nuxt.js examples for SSR.

Articles Composable

typescript
// composables/useArticles.ts
import { ref, computed } from 'vue';

export interface Article {
  id: string;
  slug: string;
  title: string;
  metaTitle: string;
  metaDescription: string;
  contentHtml: string;
  publishedAt: string;
  featuredImageUrl?: string;
}

const API_URL = import.meta.env.VITE_SEOSNIPER_API_URL;
const API_TOKEN = import.meta.env.VITE_SEOSNIPER_API_TOKEN;

const articles = ref<Article[]>([]);
const loading = ref(false);
const error = ref<Error | null>(null);

export function useArticles() {
  const fetchArticles = async () => {
    if (articles.value.length > 0) return; // Use cache

    loading.value = true;
    error.value = null;

    try {
      const response = await fetch(
        `${API_URL}/api/v1/articles`,
        {
          headers: {
            'Authorization': `Bearer ${API_TOKEN}`
          }
        }
      );

      if (!response.ok) {
        throw new Error('Failed to fetch articles');
      }

      const data = await response.json();
      articles.value = data.articles;
    } catch (err) {
      error.value = err as Error;
    } finally {
      loading.value = false;
    }
  };

  const getArticleBySlug = (slug: string) => {
    return computed(() =>
      articles.value.find((a) => a.slug === slug)
    );
  };

  return {
    articles,
    loading,
    error,
    fetchArticles,
    getArticleBySlug
  };
}

Blog List Component

vue
<!-- views/BlogList.vue -->
<script setup lang="ts">
import { onMounted } from 'vue';
import { RouterLink } from 'vue-router';
import { useArticles } from '@/composables/useArticles';

const { articles, loading, error, fetchArticles } = useArticles();

onMounted(() => {
  fetchArticles();
});
</script>

<template>
  <div class="max-w-4xl mx-auto py-12 px-4">
    <h1 class="text-4xl font-bold mb-8">Blog</h1>

    <div v-if="loading" class="text-center py-8">
      Loading articles...
    </div>

    <div v-else-if="error" class="text-red-500">
      Error: {{ error.message }}
    </div>

    <div v-else class="grid gap-8">
      <article
        v-for="article in articles"
        :key="article.id"
        class="border rounded-lg overflow-hidden"
      >
        <img
          v-if="article.featuredImageUrl"
          :src="article.featuredImageUrl"
          :alt="article.title"
          class="w-full h-48 object-cover"
        />
        <div class="p-6">
          <RouterLink :to="`/blog/${article.slug}`">
            <h2 class="text-2xl font-semibold hover:text-blue-600">
              {{ article.title }}
            </h2>
          </RouterLink>
          <p class="text-gray-600 mt-2">
            {{ article.metaDescription }}
          </p>
          <time class="text-sm text-gray-500 mt-4 block">
            {{ new Date(article.publishedAt).toLocaleDateString() }}
          </time>
        </div>
      </article>
    </div>
  </div>
</template>

Article Page Component

vue
<!-- views/ArticlePage.vue -->
<script setup lang="ts">
import { onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useHead } from '@vueuse/head';
import { useArticles } from '@/composables/useArticles';

const route = useRoute();
const { fetchArticles, getArticleBySlug } = useArticles();

const slug = route.params.slug as string;
const article = getArticleBySlug(slug);

onMounted(() => {
  fetchArticles();
});

// Update meta tags when article loads
watch(article, (newArticle) => {
  if (newArticle) {
    useHead({
      title: newArticle.metaTitle,
      meta: [
        { name: 'description', content: newArticle.metaDescription },
        { property: 'og:title', content: newArticle.metaTitle },
        { property: 'og:description', content: newArticle.metaDescription },
        ...(newArticle.featuredImageUrl ? [
          { property: 'og:image', content: newArticle.featuredImageUrl }
        ] : [])
      ]
    });
  }
});
</script>

<template>
  <article v-if="article" class="max-w-3xl mx-auto py-12 px-4">
    <header class="mb-8">
      <h1 class="text-4xl font-bold mb-4">{{ article.title }}</h1>
      <time class="text-gray-500">
        {{ new Date(article.publishedAt).toLocaleDateString() }}
      </time>
    </header>

    <!-- Render HTML content -->
    <div
      class="prose prose-lg max-w-none"
      v-html="article.contentHtml"
    />
  </article>

  <div v-else class="text-center py-12">
    Article not found
  </div>
</template>

Vue Router Configuration

typescript
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import BlogList from '@/views/BlogList.vue';
import ArticlePage from '@/views/ArticlePage.vue';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/blog', name: 'blog', component: BlogList },
    { path: '/blog/:slug', name: 'article', component: ArticlePage }
  ]
});

export default router;
SEO Sniper