Vue templateFree

Blog Index

Vue 3 blog home template with a featured post, drawn cover art, category chips, live search, pagination and a newsletter signup. Tailwind CSS with dark mode.

by Vueplayv1.0.0Updated 2 hours ago

Install
Full screen ↗

About this template

A complete home page for a company or engineering blog. A featured article leads, followed by a filterable grid of posts, a newsletter call-out and a simple footer. Every cover is drawn with SVG and gradients, so there are no image files to manage until you are ready to add your own.

Filtering is plain Vue: category chips and a search box feed a computed list that is paginated six posts at a time. Chips show a live count, the search matches titles, excerpts and authors, and an empty state offers a one-click reset.

Posts live in src/data.ts as typed objects, so swapping in content from a CMS or Markdown files means replacing one array. Change the accent by searching for rose- and the brand name in the header and footer.

  • Featured post, post grid and newsletter section
  • Category filter chips with counts and live search
  • Paginated results with empty state
  • Six SVG cover-art patterns, no image assets
  • Responsive header with mobile menu, light and dark mode

Code

20 files in the repo
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { categories, posts, type Category } from './data'
import BlogFooter from './components/BlogFooter.vue'
import BlogHeader from './components/BlogHeader.vue'
import FeaturedPost from './components/FeaturedPost.vue'
import NewsletterBox from './components/NewsletterBox.vue'
import PaginationNav from './components/PaginationNav.vue'
import PostCard from './components/PostCard.vue'
import PostFilters from './components/PostFilters.vue'

const PAGE_SIZE = 6

const featured = posts[0]!
const category = ref<Category | 'All'>('All')
const query = ref('')
const page = ref(1)

const isFiltering = computed(() => category.value !== 'All' || query.value.trim() !== '')

const counts = computed(() => {
  const result: Record<string, number> = { All: posts.length }
  for (const post of posts) result[post.category] = (result[post.category] ?? 0) + 1
  return result
})

const results = computed(() => {
  const q = query.value.trim().toLowerCase()
  return posts.filter((post) => {
    if (!isFiltering.value && post.id === featured.id) return false
    if (category.value !== 'All' && post.category !== category.value) return false
    if (!q) return true
    return [post.title, post.excerpt, post.author.name, post.category].some(text => text.toLowerCase().includes(q))
  })
})

const totalPages = computed(() => Math.max(1, Math.ceil(results.value.length / PAGE_SIZE)))
const visible = computed(() => results.value.slice((page.value - 1) * PAGE_SIZE, page.value * PAGE_SIZE))

watch([category, query], () => { page.value = 1 })

const postsSection = ref<HTMLElement>()
function goToPage(n: number) {
  page.value = n
  postsSection.value?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}

function reset() {
  category.value = 'All'
  query.value = ''
}
</script>

<template>
  <div class="min-h-screen bg-white text-gray-900 dark:bg-gray-950 dark:text-white">
    <BlogHeader />

    <main class="mx-auto max-w-6xl px-4 sm:px-6">
      <section class="pt-14 pb-10 sm:pt-20" aria-labelledby="blog-title">
        <p class="text-sm font-semibold text-rose-600 dark:text-rose-400">The Basalt blog</p>
        <h1 id="blog-title" class="mt-2 max-w-3xl text-4xl font-bold tracking-tight text-balance sm:text-5xl">Notes from the team building Basalt</h1>
        <p class="mt-4 max-w-2xl text-lg text-gray-600 dark:text-gray-400">Deep dives on how we design, build and run a product used by thousands of teams, written by the engineers, designers and PMs who did the work.</p>
      </section>

      <FeaturedPost v-if="!isFiltering" :post="featured" />

      <section id="posts" ref="postsSection" class="scroll-mt-24 pt-16 pb-16" aria-labelledby="posts-title">
        <h2 id="posts-title" class="sr-only">All articles</h2>
        <PostFilters v-model:category="category" v-model:query="query" :categories="categories" :counts="counts" />

        <p class="mt-6 text-sm text-gray-500 dark:text-gray-400" aria-live="polite">
          <template v-if="isFiltering">{{ results.length }} {{ results.length === 1 ? 'article' : 'articles' }} found</template>
          <template v-else>Latest articles</template>
        </p>

        <div v-if="visible.length" class="mt-6 grid gap-x-8 gap-y-12 sm:grid-cols-2 lg:grid-cols-3">
          <PostCard v-for="post in visible" :key="post.id" :post="post" />
        </div>

        <div v-else class="mt-6 rounded-2xl border border-dashed border-gray-300 px-6 py-16 text-center dark:border-white/15">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" class="mx-auto size-10 text-gray-400" aria-hidden="true">
            <circle cx="11" cy="11" r="6.5" /><path d="m16 16 4 4M8.5 11h5" />
          </svg>
          <p class="mt-4 font-semibold">No articles match “{{ query }}”</p>
          <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Try a different keyword or browse all categories.</p>
          <button type="button" class="mt-5 rounded-lg px-3 py-2 text-sm font-semibold text-rose-600 hover:bg-rose-50 focus-visible:outline-2 focus-visible:outline-rose-500 dark:text-rose-400 dark:hover:bg-rose-500/10" @click="reset">Clear filters</button>
        </div>

        <PaginationNav class="mt-14" :model-value="page" :total-pages="totalPages" @update:model-value="goToPage" />
      </section>

      <NewsletterBox class="mb-20" />
    </main>

    <BlogFooter />
  </div>
</template>
All blogs →
Preview of Blog ArticleFree

Template · Blogs

Blog Article

Long-form Vue 3 article template with hand-tuned Tailwind typography, a scroll-aware table of contents, code blocks, callouts, author box and related posts.

by Vueplay
Preview of Docs Layout$15

Template · Documentation

Docs Layout

Premium Vue 3 documentation template: searchable sidebar, content pages, scroll-aware table of contents, tabbed code blocks with copy, callouts and a mobile drawer.

by Vueplay
Preview of 404 Not Found PageFree

Template · Error pages

404 Not Found Page

Friendly Vue 3 404 page with a drawn compass illustration, live search suggestions and helpful links, plus a compact 500 error variant. Tailwind CSS.

by Vueplay
Preview of Admin Dashboard$29

Template · Dashboards

Admin Dashboard

A complete Vue 3 admin dashboard template with a collapsible sidebar, hand-drawn SVG charts, a sortable customers table and settings. Tailwind CSS, dark mode.

by Vueplay

Pro feature

Upgrade to Pro

Upgrade to unlock more of Vueplay. Cancel any time.

  • Custom domains with HTTPS
  • Private projects
  • No "Made with Vueplay" badge
  • GitHub sync

$120/year

Compare plansBilling settings