mirror of
https://github.com/andwati/personal-website.git
synced 2026-10-18 02:32:54 +00:00
feat: Rearchitect personal website with new pages, blog functionality using Nuxt UI and Nuxt Content.
This commit is contained in:
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
},
|
||||
"editor.quickSuggestions": {
|
||||
"strings": true
|
||||
}
|
||||
}
|
||||
+103
-2
@@ -1,4 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import type { NavigationMenuItem } from "@nuxt/ui";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const navItems = computed<NavigationMenuItem[]>(() => [
|
||||
{
|
||||
label: "About",
|
||||
to: "/about",
|
||||
active: route.path === "/about",
|
||||
},
|
||||
{
|
||||
label: "Blog",
|
||||
to: "/blog",
|
||||
active: route.path.startsWith("/blog"),
|
||||
},
|
||||
{
|
||||
label: "Projects",
|
||||
to: "/#projects",
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
label: "Contact",
|
||||
to: "/#contact",
|
||||
active: false,
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtRouteAnnouncer />
|
||||
<NuxtPage />
|
||||
<UApp>
|
||||
<UHeader>
|
||||
<template #title>
|
||||
<NuxtLink to="/" class="flex items-center gap-2">
|
||||
<span class="inline-flex items-center justify-center size-8 rounded-full bg-primary-600 text-white text-sm font-bold">IW</span>
|
||||
<div class="flex flex-col leading-tight">
|
||||
<span class="font-semibold text-sm">Ian Wati</span>
|
||||
<span class="text-xs text-(--ui-text-muted)">Software Engineer</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<UNavigationMenu :items="navItems" />
|
||||
|
||||
<template #right>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
to="https://github.com/andwati"
|
||||
target="_blank"
|
||||
icon="i-simple-icons-github"
|
||||
aria-label="GitHub"
|
||||
/>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
to="https://x.com"
|
||||
target="_blank"
|
||||
icon="i-simple-icons-x"
|
||||
aria-label="X"
|
||||
/>
|
||||
<UColorModeButton />
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<UNavigationMenu
|
||||
:items="navItems"
|
||||
orientation="vertical"
|
||||
class="-mx-2.5"
|
||||
/>
|
||||
</template>
|
||||
</UHeader>
|
||||
|
||||
<UMain>
|
||||
<NuxtPage />
|
||||
</UMain>
|
||||
|
||||
<UFooter>
|
||||
<template #left>
|
||||
<p class="text-sm text-(--ui-text-muted)">
|
||||
© {{ new Date().getFullYear() }} Ian Wati. Built with Nuxt.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
to="https://github.com/andwati"
|
||||
target="_blank"
|
||||
icon="i-simple-icons-github"
|
||||
aria-label="GitHub"
|
||||
/>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
to="https://x.com"
|
||||
target="_blank"
|
||||
icon="i-simple-icons-x"
|
||||
aria-label="X"
|
||||
/>
|
||||
</template>
|
||||
</UFooter>
|
||||
</UApp>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@source "../../../content/**/*";
|
||||
|
||||
@theme {
|
||||
--color-primary-50: #f0fdf4;
|
||||
--color-primary-100: #dcfce7;
|
||||
--color-primary-200: #bbf7d0;
|
||||
--color-primary-300: #86efac;
|
||||
--color-primary-400: #4ade80;
|
||||
--color-primary-500: #22c55e;
|
||||
--color-primary-600: #16a34a;
|
||||
--color-primary-700: #15803d;
|
||||
--color-primary-800: #166534;
|
||||
--color-primary-900: #14532d;
|
||||
--color-primary-950: #052e16;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<template>
|
||||
<div class="alert" :style="{ 'border-color': color }">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { color } = defineProps({
|
||||
color: {
|
||||
type: String,
|
||||
default: "orange",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.alert {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border: 2px solid;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.alert:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
</style>
|
||||
@@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3>Counter: {{ count }}</h3>
|
||||
<button @click="increment">
|
||||
Increment
|
||||
</button>
|
||||
<button @click="decrement">
|
||||
Decrement
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const count = ref(0);
|
||||
|
||||
const increment = () => {
|
||||
count.value++;
|
||||
};
|
||||
|
||||
const decrement = () => {
|
||||
count.value--;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
|
||||
const { data: page } = await useAsyncData("page-" + route.path, () => {
|
||||
return queryCollection("content").path(route.path).first();
|
||||
});
|
||||
|
||||
if (!page.value) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Page not found",
|
||||
fatal: true,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentRenderer
|
||||
v-if="page"
|
||||
:value="page"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
useSeoMeta({
|
||||
title: "About — Ian Wati",
|
||||
description:
|
||||
"Learn more about Ian Wati — software engineer passionate about building great products.",
|
||||
});
|
||||
|
||||
const skills = [
|
||||
{ name: "TypeScript", icon: "i-simple-icons-typescript" },
|
||||
{ name: "Vue.js", icon: "i-simple-icons-vuedotjs" },
|
||||
{ name: "Nuxt", icon: "i-simple-icons-nuxtdotjs" },
|
||||
{ name: "Node.js", icon: "i-simple-icons-nodedotjs" },
|
||||
{ name: "Go", icon: "i-simple-icons-go" },
|
||||
{ name: "Python", icon: "i-simple-icons-python" },
|
||||
{ name: "Docker", icon: "i-simple-icons-docker" },
|
||||
{ name: "Git", icon: "i-simple-icons-git" },
|
||||
{ name: "PostgreSQL", icon: "i-simple-icons-postgresql" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Hero -->
|
||||
<section class="relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-primary-50 via-white to-primary-100/50 dark:from-primary-950/20 dark:via-(--ui-bg) dark:to-primary-900/10" />
|
||||
|
||||
<UContainer class="relative py-16 lg:py-24">
|
||||
<div class="flex flex-col sm:flex-row items-center sm:items-start gap-8 max-w-3xl">
|
||||
<img
|
||||
src="https://avatars.githubusercontent.com/u/andwati"
|
||||
alt="Ian Wati"
|
||||
class="rounded-full size-32 ring-4 ring-primary-200 dark:ring-primary-800 shadow-lg flex-shrink-0"
|
||||
/>
|
||||
<div class="space-y-4 text-center sm:text-left">
|
||||
<p class="text-xs font-semibold tracking-[0.2em] uppercase text-(--ui-text-muted)">About</p>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold">Ian Wati</h1>
|
||||
<p class="text-lg text-(--ui-text-muted) leading-relaxed">
|
||||
I'm a software engineer who loves building tools and products that make a difference.
|
||||
My interests span full-stack development, open source, and exploring new technologies.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</UContainer>
|
||||
</section>
|
||||
|
||||
<!-- What I Do -->
|
||||
<section class="py-16 lg:py-24">
|
||||
<UContainer>
|
||||
<div class="max-w-3xl mx-auto space-y-12">
|
||||
<div>
|
||||
<p class="text-xs font-semibold tracking-[0.2em] uppercase text-primary-600 dark:text-primary-400 mb-3">Focus</p>
|
||||
<h2 class="text-2xl font-bold mb-4">What I Do</h2>
|
||||
<p class="text-(--ui-text-muted) leading-relaxed">
|
||||
I specialize in building modern web applications using cutting-edge technologies.
|
||||
Whether it's crafting a seamless user experience on the frontend or architecting
|
||||
robust backend systems, I enjoy the entire spectrum of software development.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<USeparator />
|
||||
|
||||
<!-- Skills -->
|
||||
<div>
|
||||
<p class="text-xs font-semibold tracking-[0.2em] uppercase text-primary-600 dark:text-primary-400 mb-3">Skills</p>
|
||||
<h2 class="text-2xl font-bold mb-6">Technologies</h2>
|
||||
<div class="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
<div
|
||||
v-for="skill in skills"
|
||||
:key="skill.name"
|
||||
class="flex flex-col items-center gap-2 p-4 rounded-xl bg-(--ui-bg-elevated) border border-(--ui-border) hover:border-primary-300 dark:hover:border-primary-700 transition-colors"
|
||||
>
|
||||
<UIcon :name="skill.icon" class="size-8 text-primary-600 dark:text-primary-400" />
|
||||
<span class="text-xs font-medium text-(--ui-text-muted)">{{ skill.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<USeparator />
|
||||
|
||||
<!-- Contact -->
|
||||
<div>
|
||||
<p class="text-xs font-semibold tracking-[0.2em] uppercase text-primary-600 dark:text-primary-400 mb-3">Connect</p>
|
||||
<h2 class="text-2xl font-bold mb-4">Get in Touch</h2>
|
||||
<p class="text-(--ui-text-muted) leading-relaxed mb-6">
|
||||
Feel free to reach out through any of the links below. Always happy to
|
||||
connect and discuss interesting projects or ideas.
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<UButton
|
||||
label="GitHub"
|
||||
to="https://github.com/andwati"
|
||||
target="_blank"
|
||||
icon="i-simple-icons-github"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
/>
|
||||
<UButton
|
||||
label="X / Twitter"
|
||||
to="https://x.com"
|
||||
target="_blank"
|
||||
icon="i-simple-icons-x"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
/>
|
||||
<UButton
|
||||
label="Email me"
|
||||
to="mailto:[email protected]"
|
||||
icon="i-lucide-mail"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UContainer>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
|
||||
const { data: post } = await useAsyncData("blog-post-" + route.path, () =>
|
||||
queryCollection("blog").path(route.path).first(),
|
||||
);
|
||||
|
||||
if (!post.value) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Blog post not found",
|
||||
fatal: true,
|
||||
});
|
||||
}
|
||||
|
||||
useSeoMeta({
|
||||
title: post.value?.title,
|
||||
description: post.value?.description,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage v-if="post">
|
||||
<UPageHeader
|
||||
:title="post.title"
|
||||
:description="post.description"
|
||||
>
|
||||
<template #headline>
|
||||
<UBreadcrumb
|
||||
:items="[
|
||||
{ label: 'Blog', to: '/blog', icon: 'i-lucide-book-open' },
|
||||
{ label: post.title || '' }
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #bottom>
|
||||
<div class="flex items-center gap-3 text-sm text-(--ui-text-muted)">
|
||||
<UIcon name="i-lucide-calendar" />
|
||||
<time v-if="post.date">{{ new Date(post.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) }}</time>
|
||||
</div>
|
||||
</template>
|
||||
</UPageHeader>
|
||||
|
||||
<UPageBody>
|
||||
<UContainer>
|
||||
<div class="prose prose-primary dark:prose-invert max-w-none">
|
||||
<ContentRenderer :value="post" />
|
||||
</div>
|
||||
</UContainer>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
const { data: posts } = await useAsyncData("blog-posts", () =>
|
||||
queryCollection("blog").order("date", "DESC").all(),
|
||||
);
|
||||
|
||||
useSeoMeta({
|
||||
title: "Blog — Ian Wati",
|
||||
description:
|
||||
"Thoughts on software development, technology, and the things I learn along the way.",
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageHero
|
||||
title="Blog"
|
||||
description="Thoughts on software development, technology, and the things I learn along the way."
|
||||
/>
|
||||
|
||||
<UPageBody>
|
||||
<UContainer>
|
||||
<UBlogPosts v-if="posts?.length">
|
||||
<UBlogPost
|
||||
v-for="post in posts"
|
||||
:key="post.path"
|
||||
:title="post.title"
|
||||
:description="post.description"
|
||||
:date="post.date"
|
||||
:image="post.image"
|
||||
:badge="post.badge"
|
||||
:to="post.path"
|
||||
/>
|
||||
</UBlogPosts>
|
||||
|
||||
<div v-else class="text-center py-12">
|
||||
<p class="text-(--ui-text-muted)">
|
||||
No blog posts yet. Check back soon!
|
||||
</p>
|
||||
</div>
|
||||
</UContainer>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
@@ -0,0 +1,246 @@
|
||||
<script setup lang="ts">
|
||||
const { data: recentPosts } = await useAsyncData("recent-posts", () =>
|
||||
queryCollection("blog").order("date", "DESC").limit(3).all(),
|
||||
);
|
||||
|
||||
useSeoMeta({
|
||||
title: "Ian Wati — Software Engineer",
|
||||
description:
|
||||
"Software engineer passionate about building great products. I write about development, technology, and the things I learn along the way.",
|
||||
});
|
||||
|
||||
const codeSnippet = `const softwareEngineer = {
|
||||
name: 'Ian Wati',
|
||||
role: 'Software Engineer',
|
||||
skills: [
|
||||
'TypeScript', 'Vue.js', 'Nuxt',
|
||||
'Node.js', 'Go', 'Python'
|
||||
],
|
||||
getProjects: async () => {
|
||||
return await fetchProjects();
|
||||
},
|
||||
};`;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Hero Section -->
|
||||
<section class="relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-primary-50 via-white to-primary-100/50 dark:from-primary-950/20 dark:via-(--ui-bg) dark:to-primary-900/10" />
|
||||
|
||||
<UContainer class="relative py-16 lg:py-24">
|
||||
<div class="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
|
||||
<!-- Left: Text -->
|
||||
<div class="space-y-6">
|
||||
<p class="text-xs font-semibold tracking-[0.2em] uppercase text-(--ui-text-muted)">
|
||||
Software Engineer
|
||||
</p>
|
||||
|
||||
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold leading-tight">
|
||||
Hi, I'm <span class="text-primary-600 dark:text-primary-400">Ian Wati</span>
|
||||
</h1>
|
||||
|
||||
<p class="text-lg sm:text-xl text-(--ui-text-muted) leading-relaxed max-w-xl">
|
||||
I build modern web applications and love crafting software that makes a real impact.
|
||||
Passionate about clean code, open source, and continuous learning.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-3 pt-2">
|
||||
<UButton
|
||||
label="Read the Blog"
|
||||
to="/blog"
|
||||
icon="i-lucide-book-open"
|
||||
size="xl"
|
||||
/>
|
||||
<UButton
|
||||
label="Get in Touch"
|
||||
to="#contact"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-mail"
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 pt-2 text-sm text-(--ui-text-muted)">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<UIcon name="i-lucide-map-pin" class="size-4" />
|
||||
Nairobi, Kenya
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<UIcon name="i-lucide-mail" class="size-4" />
|
||||
hello@ianwati.dev
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Code Editor -->
|
||||
<div class="relative hidden lg:block">
|
||||
<!-- Floating stat cards -->
|
||||
<div class="absolute -top-4 -right-2 z-10">
|
||||
<div class="bg-primary-800 text-white rounded-xl px-4 py-3 shadow-lg flex items-center gap-3">
|
||||
<UIcon name="i-lucide-git-commit-horizontal" class="size-5 text-primary-300" />
|
||||
<div>
|
||||
<p class="font-bold text-sm">Open Source</p>
|
||||
<p class="text-xs text-primary-200">Active Contributor</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="absolute -bottom-2 -right-4 z-10">
|
||||
<div class="bg-primary-700 text-white rounded-xl px-4 py-3 shadow-lg flex items-center gap-3">
|
||||
<UIcon name="i-lucide-code-2" class="size-5 text-primary-300" />
|
||||
<div>
|
||||
<p class="font-bold text-sm">Full Stack</p>
|
||||
<p class="text-xs text-primary-200">Frontend & Backend</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Code window -->
|
||||
<div class="bg-primary-950 dark:bg-gray-900 rounded-xl shadow-2xl overflow-hidden ring ring-primary-800/20">
|
||||
<div class="flex items-center gap-2 px-4 py-3 border-b border-white/10">
|
||||
<div class="flex gap-1.5">
|
||||
<span class="size-3 rounded-full bg-red-500" />
|
||||
<span class="size-3 rounded-full bg-yellow-500" />
|
||||
<span class="size-3 rounded-full bg-green-500" />
|
||||
</div>
|
||||
<span class="text-xs text-white/40 ml-2">portfolio.ts</span>
|
||||
</div>
|
||||
<pre class="p-5 text-sm font-mono leading-relaxed overflow-x-auto"><code class="text-green-400">{{ codeSnippet }}</code></pre>
|
||||
<div class="flex items-center justify-between px-4 py-2 border-t border-white/10 text-xs text-white/40">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="size-2 rounded-full bg-green-500" />
|
||||
Online and coding
|
||||
</span>
|
||||
<span>Last commit: Today</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UContainer>
|
||||
</section>
|
||||
|
||||
<!-- About Section -->
|
||||
<section id="about" class="py-16 lg:py-24">
|
||||
<UContainer>
|
||||
<div class="text-center mb-12">
|
||||
<p class="text-xs font-semibold tracking-[0.2em] uppercase text-primary-600 dark:text-primary-400 mb-3">About</p>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold">What I Do</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<UCard>
|
||||
<div class="space-y-3">
|
||||
<div class="inline-flex items-center justify-center size-10 rounded-lg bg-primary-100 dark:bg-primary-900/30">
|
||||
<UIcon name="i-lucide-monitor" class="size-5 text-primary-600 dark:text-primary-400" />
|
||||
</div>
|
||||
<h3 class="font-semibold text-lg">Frontend Development</h3>
|
||||
<p class="text-sm text-(--ui-text-muted) leading-relaxed">Building responsive, accessible, and performant user interfaces with Vue.js, Nuxt, and modern CSS.</p>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<div class="space-y-3">
|
||||
<div class="inline-flex items-center justify-center size-10 rounded-lg bg-primary-100 dark:bg-primary-900/30">
|
||||
<UIcon name="i-lucide-server" class="size-5 text-primary-600 dark:text-primary-400" />
|
||||
</div>
|
||||
<h3 class="font-semibold text-lg">Backend Development</h3>
|
||||
<p class="text-sm text-(--ui-text-muted) leading-relaxed">Designing robust APIs and services with Node.js, Go, and Python. Focused on scalability and reliability.</p>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<div class="space-y-3">
|
||||
<div class="inline-flex items-center justify-center size-10 rounded-lg bg-primary-100 dark:bg-primary-900/30">
|
||||
<UIcon name="i-lucide-lightbulb" class="size-5 text-primary-600 dark:text-primary-400" />
|
||||
</div>
|
||||
<h3 class="font-semibold text-lg">Open Source</h3>
|
||||
<p class="text-sm text-(--ui-text-muted) leading-relaxed">Contributing to open source projects and building tools that help the developer community grow.</p>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
</UContainer>
|
||||
</section>
|
||||
|
||||
<!-- Recent Blog Posts Section -->
|
||||
<section class="py-16 lg:py-24 bg-(--ui-bg-elevated)">
|
||||
<UContainer>
|
||||
<div class="flex items-end justify-between mb-12">
|
||||
<div>
|
||||
<p class="text-xs font-semibold tracking-[0.2em] uppercase text-primary-600 dark:text-primary-400 mb-3">Blog</p>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold">Latest Posts</h2>
|
||||
<p class="mt-2 text-(--ui-text-muted)">Thoughts on development, technology, and things I learn along the way.</p>
|
||||
</div>
|
||||
<UButton
|
||||
label="View All"
|
||||
to="/blog"
|
||||
variant="ghost"
|
||||
trailing-icon="i-lucide-arrow-right"
|
||||
class="hidden sm:inline-flex"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UBlogPosts v-if="recentPosts?.length">
|
||||
<UBlogPost
|
||||
v-for="post in recentPosts"
|
||||
:key="post.path"
|
||||
:title="post.title"
|
||||
:description="post.description"
|
||||
:date="post.date"
|
||||
:image="post.image"
|
||||
:badge="post.badge"
|
||||
:to="post.path"
|
||||
/>
|
||||
</UBlogPosts>
|
||||
|
||||
<div v-else class="text-center py-12">
|
||||
<p class="text-(--ui-text-muted)">No blog posts yet. Stay tuned!</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 text-center sm:hidden">
|
||||
<UButton label="View All Posts" to="/blog" variant="subtle" trailing-icon="i-lucide-arrow-right" />
|
||||
</div>
|
||||
</UContainer>
|
||||
</section>
|
||||
|
||||
<!-- Contact Section -->
|
||||
<section id="contact" class="py-16 lg:py-24">
|
||||
<UContainer>
|
||||
<div class="max-w-2xl mx-auto text-center space-y-6">
|
||||
<p class="text-xs font-semibold tracking-[0.2em] uppercase text-primary-600 dark:text-primary-400">Contact</p>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold">Get in Touch</h2>
|
||||
<p class="text-lg text-(--ui-text-muted)">
|
||||
Have a project in mind or just want to connect? Feel free to reach out.
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center gap-4 pt-4">
|
||||
<UButton
|
||||
label="Email me"
|
||||
to="mailto:[email protected]"
|
||||
icon="i-lucide-mail"
|
||||
size="xl"
|
||||
/>
|
||||
<UButton
|
||||
label="GitHub"
|
||||
to="https://github.com/andwati"
|
||||
target="_blank"
|
||||
icon="i-simple-icons-github"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xl"
|
||||
/>
|
||||
<UButton
|
||||
label="X / Twitter"
|
||||
to="https://x.com"
|
||||
target="_blank"
|
||||
icon="i-simple-icons-x"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</UContainer>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
+10
-1
@@ -1,4 +1,4 @@
|
||||
import { defineContentConfig, defineCollection } from "@nuxt/content";
|
||||
import { defineContentConfig, defineCollection, z } from "@nuxt/content";
|
||||
|
||||
export default defineContentConfig({
|
||||
collections: {
|
||||
@@ -6,5 +6,14 @@ export default defineContentConfig({
|
||||
type: "page",
|
||||
source: "**",
|
||||
}),
|
||||
blog: defineCollection({
|
||||
type: "page",
|
||||
source: "blog/**",
|
||||
schema: z.object({
|
||||
date: z.string(),
|
||||
image: z.string().optional(),
|
||||
badge: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# About Content Version 3
|
||||
|
||||
[Back home](/)
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: "Building a Blog with Nuxt Content"
|
||||
description: "Learn how to use Nuxt Content v3 to create a powerful, file-based blog with markdown support."
|
||||
date: "2025-02-01"
|
||||
image: "https://images.unsplash.com/photo-1499750310107-5fef28a66643?w=800&h=400&fit=crop"
|
||||
badge: "Tutorial"
|
||||
---
|
||||
|
||||
# Building a Blog with Nuxt Content
|
||||
|
||||
Nuxt Content v3 is a powerful module that turns your Nuxt application into a git-based CMS. In this post, we'll explore how to set up a blog using Nuxt Content.
|
||||
|
||||
## Why Nuxt Content?
|
||||
|
||||
Nuxt Content provides several advantages for content management:
|
||||
|
||||
- **Markdown support** — Write your content in Markdown with full MDC syntax support
|
||||
- **Collections** — Organize your content into typed collections with schema validation
|
||||
- **Querying** — Powerful query API to filter, sort, and paginate your content
|
||||
- **Vue components in Markdown** — Embed Vue components directly in your markdown files
|
||||
|
||||
## Setting Up Collections
|
||||
|
||||
Content collections are defined in your `content.config.ts` file:
|
||||
|
||||
```typescript
|
||||
import { defineContentConfig, defineCollection, z } from "@nuxt/content";
|
||||
|
||||
export default defineContentConfig({
|
||||
collections: {
|
||||
blog: defineCollection({
|
||||
type: "page",
|
||||
source: "blog/**",
|
||||
schema: z.object({
|
||||
date: z.string(),
|
||||
image: z.string().optional(),
|
||||
badge: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Querying Content
|
||||
|
||||
You can query your blog posts using the `queryCollection` composable:
|
||||
|
||||
```typescript
|
||||
const { data: posts } = await useAsyncData("posts", () =>
|
||||
queryCollection("blog").order("date", "DESC").all()
|
||||
);
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
Nuxt Content makes it incredibly easy to build a content-driven website. With collections, schema validation, and the powerful query API, you have all the tools you need to create a great blog experience.
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: "Getting Started with Nuxt 4"
|
||||
description: "An introduction to Nuxt 4 and its new features that make web development even more enjoyable."
|
||||
date: "2025-01-15"
|
||||
image: "https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=800&h=400&fit=crop"
|
||||
badge: "Nuxt"
|
||||
---
|
||||
|
||||
# Getting Started with Nuxt 4
|
||||
|
||||
Nuxt 4 represents a significant evolution in the Nuxt ecosystem. With a refined project structure, improved developer experience, and powerful new features, it's never been a better time to start building with Nuxt.
|
||||
|
||||
## What's New in Nuxt 4
|
||||
|
||||
Nuxt 4 brings several exciting improvements:
|
||||
|
||||
- **New directory structure** — The `app/` directory is now the default home for your Vue application code
|
||||
- **Improved TypeScript support** — Better type inference and auto-completion out of the box
|
||||
- **Enhanced performance** — Faster builds and smaller bundle sizes
|
||||
- **Better data fetching** — Reworked async data layer for more predictable behavior
|
||||
|
||||
## Getting Started
|
||||
|
||||
Setting up a new Nuxt 4 project is straightforward:
|
||||
|
||||
```bash
|
||||
npx nuxi@latest init my-app
|
||||
```
|
||||
|
||||
This creates a fresh project with all the latest defaults. From there, you can start building your application right away.
|
||||
|
||||
## Why Nuxt?
|
||||
|
||||
Nuxt takes the best of Vue.js and adds powerful conventions and features on top:
|
||||
|
||||
1. **File-based routing** — No manual route configuration needed
|
||||
2. **Auto-imports** — Components, composables, and utilities are automatically available
|
||||
3. **Server-side rendering** — Built-in SSR for better performance and SEO
|
||||
4. **Modules ecosystem** — Rich ecosystem of modules for common needs
|
||||
|
||||
Whether you're building a simple landing page or a complex full-stack application, Nuxt provides the tools and conventions to do it efficiently.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Nuxt 4 is a fantastic framework for modern web development. Give it a try and see how it can streamline your workflow!
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: "Why I Love Open Source"
|
||||
description: "Reflections on the open source community and why contributing to open source projects is so rewarding."
|
||||
date: "2025-02-10"
|
||||
image: "https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&h=400&fit=crop"
|
||||
badge: "Thoughts"
|
||||
---
|
||||
|
||||
# Why I Love Open Source
|
||||
|
||||
Open source software has transformed the way we build technology. As a developer, contributing to open source projects has been one of the most rewarding aspects of my career.
|
||||
|
||||
## The Power of Community
|
||||
|
||||
The open source community is incredibly diverse and welcoming. Some of the best learning experiences come from:
|
||||
|
||||
- **Code reviews** — Getting feedback from experienced developers
|
||||
- **Issue discussions** — Understanding real-world problems and solutions
|
||||
- **Collaboration** — Working with people from around the world
|
||||
|
||||
## Giving Back
|
||||
|
||||
Contributing to open source isn't just about writing code. There are many ways to help:
|
||||
|
||||
1. **Documentation** — Good docs make projects accessible to everyone
|
||||
2. **Bug reports** — Detailed bug reports save maintainers hours of work
|
||||
3. **Testing** — Trying out new features and providing feedback
|
||||
4. **Mentoring** — Helping newcomers get started with contributions
|
||||
|
||||
## Building in the Open
|
||||
|
||||
One of the things I enjoy most is building projects in the open. Sharing your work publicly:
|
||||
|
||||
- Forces you to write cleaner, more maintainable code
|
||||
- Creates a portfolio of your work
|
||||
- Helps others learn from your approach
|
||||
- Opens doors to collaboration and feedback
|
||||
|
||||
## Final Thoughts
|
||||
|
||||
If you haven't contributed to open source yet, I encourage you to start. Find a project you use and love, and look for ways to help. Every contribution, no matter how small, makes a difference.
|
||||
@@ -1,24 +0,0 @@
|
||||
# Welcome to Nuxt Content Starter
|
||||
|
||||
This is the main page displaying Markdown located at [content/index.md](https://github.com/nuxt/starter/blob/content/content/index.md).
|
||||
|
||||
Move to [about](/about) page.
|
||||
|
||||
## Manage your Contents
|
||||
|
||||
Create new pages or modify the existing ones in `content/` directory.
|
||||
|
||||
## Query & Render Pages
|
||||
|
||||
You can find an example of querying contents and rendering them in a [catch-all page](https://github.com/nuxt/starter/blob/content/app/pages/%5B...slug%5D.vue)
|
||||
|
||||
## Integrate Vue Component
|
||||
|
||||
::alert{color="green"}
|
||||
The current [alert](https://github.com/nuxt/starter/blob/content/app/components/Alert.vue) and the [counter](https://github.com/nuxt/starter/blob/content/app/components/Counter.vue) below are `Vue` components integrated into the Markdown.
|
||||
::
|
||||
|
||||
::counter
|
||||
::
|
||||
|
||||
Checkout out the [documentation](https://content.nuxt.com/docs/getting-started) to learn more.
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
modules: ["@nuxt/content"],
|
||||
modules: ["@nuxt/ui", "@nuxt/image", "@nuxt/content"],
|
||||
devtools: { enabled: true },
|
||||
css: ["~/assets/css/main.css"],
|
||||
compatibilityDate: "2024-04-03",
|
||||
});
|
||||
|
||||
+2
-1
@@ -14,7 +14,8 @@
|
||||
"@nuxt/image": "2.0.0",
|
||||
"@nuxt/ui": "4.4.0",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"nuxt": "^4.3.1"
|
||||
"nuxt": "^4.3.1",
|
||||
"tailwindcss": "^4.1.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.3.15"
|
||||
|
||||
Generated
+3
@@ -26,6 +26,9 @@ importers:
|
||||
nuxt:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1(@biomejs/[email protected])(@parcel/[email protected])(@vue/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])
|
||||
tailwindcss:
|
||||
specifier: ^4.1.18
|
||||
version: 4.1.18
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: 2.3.15
|
||||
|
||||
Reference in New Issue
Block a user