basicstack.org/pages/index.vue
Paperclip CTO a7bd527793 Initial commit: Move basicstack.org frontend to Forgejo
Migrate the Nuxt 3 + Vue 3 SSR frontend application for basicstack.org to our self-hosted Forgejo instance. This repository will serve as the source for all future development and deployment of the basicstack.org website.

The application includes:
- Nuxt 3 + Vue 3 SSR setup
- Directus CMS integration
- Tailwind CSS styling
- Kubernetes deployment manifests
- Docker containerization

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-28 12:32:23 +00:00

246 lines
6.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<section class="hero">
<div class="container">
<h1 class="hero-title">Production-ready open source infrastructure</h1>
<p class="hero-sub">
BasicStack is a curated collection of self-hosted open source software, deployed and maintained on Hetzner Cloud infrastructure in Germany.
</p>
<div class="hero-actions">
<NuxtLink to="/packages" class="btn btn-primary">Browse Packages</NuxtLink>
<NuxtLink to="/infrastructure" class="btn btn-outline">Our Infrastructure</NuxtLink>
</div>
</div>
</section>
<section class="packages-section" v-if="packages && packages.length > 0">
<div class="container">
<h2 class="section-title">Software Packages</h2>
<p class="section-sub">Open source tools we run and maintain</p>
<div class="packages-grid">
<NuxtLink
v-for="pkg in packages.slice(0, 6)"
:key="pkg.id"
:to="`/packages/${pkg.slug}`"
class="package-card card"
>
<img
v-if="pkg.logo"
:src="getAssetUrl(pkg.logo)"
:alt="pkg.name + ' logo'"
class="package-logo"
/>
<div v-else class="package-logo-placeholder">
{{ pkg.name[0] }}
</div>
<h3 class="package-name">{{ pkg.name }}</h3>
<p class="package-desc">{{ pkg.short_description }}</p>
</NuxtLink>
</div>
<div class="section-cta" v-if="packages.length > 6">
<NuxtLink to="/packages" class="btn btn-outline">View all {{ packages.length }} packages</NuxtLink>
</div>
</div>
</section>
<section class="empty-state" v-else-if="packages !== null">
<div class="container">
<h2 class="section-title">Software Packages</h2>
<p class="section-sub">Content coming soon...</p>
</div>
</section>
<section class="infra-section">
<div class="container">
<h2 class="section-title">Infrastructure</h2>
<p class="section-sub">How we run BasicStack</p>
<div class="infra-grid" v-if="infrastructure && infrastructure.length > 0">
<NuxtLink
v-for="item in infrastructure.slice(0, 4)"
:key="item.id"
:to="item.slug ? `/infrastructure/${item.slug}` : '/infrastructure'"
class="infra-card card"
>
<span class="infra-category badge badge-primary">{{ formatCategory(item.category) }}</span>
<h3 class="infra-title">{{ item.title }}</h3>
<p class="infra-summary" v-if="item.summary">{{ item.summary }}</p>
</NuxtLink>
</div>
<div class="section-cta">
<NuxtLink to="/infrastructure" class="btn btn-outline">Learn more about our infrastructure</NuxtLink>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
const { getPackages, getInfrastructure, getAssetUrl } = useDirectus()
const [{ data: packages }, { data: infrastructure }] = await Promise.all([
useAsyncData('home-packages', () => getPackages()),
useAsyncData('home-infrastructure', () => getInfrastructure()),
])
const categoryLabels: Record<string, string> = {
hosting: 'Hosting',
security: 'Security',
monitoring: 'Monitoring',
backup: 'Backup',
disaster_recovery: 'Disaster Recovery',
}
function formatCategory(cat: string) {
return categoryLabels[cat] ?? cat
}
useSeoMeta({
title: 'BasicStack Production-ready open source infrastructure',
description: 'A curated collection of self-hosted open source software on Hetzner Cloud infrastructure in Germany.',
ogTitle: 'BasicStack',
ogDescription: 'Production-ready open source infrastructure',
})
</script>
<style scoped>
.hero {
background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg) 100%);
padding: var(--space-3xl) 0;
text-align: center;
}
.hero-title {
font-size: clamp(2rem, 5vw, 3.5rem);
margin-bottom: var(--space-lg);
line-height: 1.15;
}
.hero-sub {
font-size: 1.125rem;
color: var(--color-text-muted);
max-width: 600px;
margin: 0 auto var(--space-xl);
}
.hero-actions {
display: flex;
gap: var(--space-md);
justify-content: center;
flex-wrap: wrap;
}
.packages-section,
.infra-section {
padding: var(--space-3xl) 0;
}
.infra-section {
background: var(--color-bg-secondary);
}
.section-title {
font-size: 1.875rem;
margin-bottom: var(--space-sm);
}
.section-sub {
color: var(--color-text-muted);
margin-bottom: var(--space-2xl);
}
.packages-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--space-lg);
}
.package-card {
text-decoration: none;
color: inherit;
transition: box-shadow 0.2s, transform 0.2s;
}
.package-card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
text-decoration: none;
}
.package-logo {
width: 64px;
height: 64px;
object-fit: contain;
margin-bottom: var(--space-md);
}
.package-logo-placeholder {
width: 64px;
height: 64px;
border-radius: var(--radius-md);
background: var(--color-primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 700;
margin-bottom: var(--space-md);
}
.package-name {
font-size: 1.125rem;
margin-bottom: var(--space-sm);
}
.package-desc {
color: var(--color-text-muted);
font-size: 0.9375rem;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.section-cta {
text-align: center;
margin-top: var(--space-2xl);
}
.infra-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: var(--space-lg);
margin-bottom: var(--space-xl);
}
.infra-card {
text-decoration: none;
color: inherit;
transition: box-shadow 0.2s;
}
.infra-card:hover {
box-shadow: var(--shadow-md);
text-decoration: none;
}
.infra-category {
margin-bottom: var(--space-md);
}
.infra-title {
font-size: 1.125rem;
margin: var(--space-sm) 0;
}
.infra-summary {
color: var(--color-text-muted);
font-size: 0.9375rem;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>