Objectives and Framework Selection
The primary goal of this project was to create a modern, fast, and SEO-friendly website to showcase my wife’s freelance event business - check it out here Paige Levy's Portfolio.
She needed an online portfolio that demonstrated her past projects and an easy way for potential clients to contact her. Performance and search engine optimization were key priorities, and that's why I chose Astro. Astro's reputation for creating lightweight, fast websites made it a natural choice for a mostly static content website that didn’t require much dynamic interactivity.
Learning Curve and Experience with Astro
I decided to kickstart the project using an Astro theme called Odyssey. It was a pre-built theme with many useful components, which I thought would help speed up development. However, I soon discovered that the theme didn’t include Tailwind CSS, which meant I had to do a lot of styling inline—a cumbersome task compared to using Tailwind’s utility-first classes. Despite this, my first experience with Astro was positive. The file organization was intuitive, and the static site generator features excelled at creating fast-loading pages. Compared to my past experiences with frameworks like Next.js and React, Astro felt more appropriate for a static content site.
Development Environment
For development, I used Cursor AI IDE, which made coding much faster and more efficient. The site is hosted on Vercel, mainly because I already have other websites hosted there, and it made managing everything simpler. I also leveraged ChatGPT for answering some technical questions and used image generation from the Flux Schnell model on Replicate for visuals. Image compression was handled by Squoosh, which helped optimize performance. 4. Project Structure Astro’s project structure was pretty easy to navigate. The Odyssey theme came with organized folders that made it easy to understand where each component belonged. This structure was especially helpful for managing the portfolio and contact pages, as everything was clearly organized into separate sections, which minimized the learning curve.
Component Development
The Odyssey theme included many pre-built components, such as a contact form and a project list, which made the setup straightforward. One of the few custom components I created was a logo cloud, which I developed using Vercel’s V0. Overall, the available components covered most of the website’s needs, and Cursor AI, utilizing the Claude Sonnet model, helped polish the design and styling of these components. 6. Styling and Design Challenges Styling was a challenge since the theme didn't come with Tailwind, which I had become accustomed to for its simplicity and efficiency. Instead, I relied on inline styles, which slowed the process a bit. Moving forward, I would consider using an Astro-Tailwind combination, as Tailwind is well-suited for rapid styling, especially when coding alongside AI tools.
import Container from '../core/Container.astro';
const logos = [
{
name: "Bitcoin",
src: "/assets/logos/bitcoin_Logo.png",
width: 150,
height: 40
},
{
name: "TBD",
src: "/assets/logos/tbd.png",
width: 150,
height: 40
},
{
name: "Block",
src: "/assets/logos/block_logo.png",
width: 120,
height: 40
},
{
name: "Filecoin",
src: "/assets/logos/filecoin-fil-logo.png",
width: 140,
height: 40
},
{
name: "Microsoft",
src: "/assets/logos/microsoft_logo.png",
width: 160,
height: 40
},
{
name: 'Sofi',
src: '/assets/logos/sofi_logo.png',
width: 100,
height: 40
}
];
interface Props {
showHeader?: boolean;
}
const { showHeader = true } = Astro.props;
---
<section class="logo-cloud__section" style={`background-color: var(--theme-surface-1);`}>
<Container narrow>
{showHeader && (
<div class="logo-cloud__header fade-in">
<h2 style="text-align: center;">Trusted By Industry Leaders</h2>
</div>
)}
<div class="logo-track">
<div class="logo-grid">
{logos.map((logo) => (
<div class="logo-container">
<img
src={logo.src}
alt={logo.name}
width={logo.width}
height={logo.height}
class="logo-image"
/>
</div>
))}
</div>
<div class="logo-grid">
{logos.map((logo) => (
<div class="logo-container">
<img
src={logo.src}
alt={logo.name}
width={logo.width}
height={logo.height}
class="logo-image"
/>
</div>
))}
</div>
</div>
</Container>
</section>
<style>
.logo-cloud__section {
margin: var(--section-margin) auto;
padding: clamp(10vh, 12vh, 18vh) 2rem;
border-radius: var(--theme-shape-radius);
}
.logo-cloud__header {
margin-bottom: 1.5rem;
}
.logo-cloud__header h2 {
font-size: var(--font-size-xl);
font-weight: 600;
line-height: 1.1;
margin-bottom: 1rem;
}
.logo-cloud__header p {
font-size: var(--font-size-lg);
color: var(--theme-text-lighter);
line-height: 1.4;
}
.logo-track {
display: flex;
width: 100%;
overflow: hidden;
gap: 4rem;
padding: 2rem 0;
}
.logo-grid {
display: flex;
gap: 2.5rem;
animation: scroll 12s linear infinite;
padding: 1rem 0;
}
.logo-track:hover .logo-grid {
animation-play-state: paused;
}
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(-100% - 4rem));
}
}
.logo-container {
display: flex;
align-items: center;
justify-content: center;
min-width: 120px;
height: 60px;
padding: 0.5rem;
transition: all 0.3s ease;
}
.logo-image {
max-width: 100%;
height: auto;
object-fit: contain;
filter: grayscale(100%) opacity(0.7);
transition: all 0.3s ease;
}
.logo-container:hover {
transform: translateY(-2px);
}
.logo-image {
max-width: 100%;
height: auto;
object-fit: contain;
filter: grayscale(100%) opacity(0.7);
transition: all 0.3s ease;
}
.logo-container:hover .logo-image {
filter: grayscale(0%) opacity(1);
}
@media (min-width: 768px) {
.logo-cloud__section {
padding: 2rem;
}
.logo-cloud__header {
margin-bottom: 2rem;
}
.logo-cloud__header h2 {
font-size: var(--font-size-md);
}
.logo-container {
width: 9rem;
height: 4.5rem;
}
}
@media (min-width: 1024px) {
.logo-cloud__section {
padding: 3rem;
}
.logo-container {
width: 10rem;
height: 5rem;
}
.logo-container:hover {
transform: scale(1.05);
}
.logo-image:hover {
filter: grayscale(0%);
}
}
@media (min-width: 1440px) {
.logo-container {
width: 12rem;
height: 6rem;
}
}
.fade-in {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.logo-grid {
animation: none;
gap: 2rem;
}
.logo-track {
gap: 2rem;
flex-wrap: wrap;
justify-content: center;
}
}
</style>
Content Management
For content management, I migrated content from the old website, making updates manually as needed. We used AI to create placeholder content, but the bulk of the initial material came from the original website. Since we decided not to use a CMS, I’ll be handling future updates myself. This approach works fine as my wife isn't particularly technical, and it simplifies maintenance.
Performance Optimization
Once the site was in production, I used Lighthouse to assess performance, accessibility, SEO, and other metrics. The initial results were excellent, with high scores across the board. I did need to make a few adjustments, particularly with image compression, but after that, the site scored very well on all fronts. Given its static nature, there was minimal layout shift, and page load speeds were fast.
Challenges and Solutions
One of the biggest challenges was trying to use Astro's image component, which wasn’t compatible with the pre-built theme. All the images were stored in the public folder, and I ran into some issues related to Vercel’s handling of dynamic imports. Despite multiple attempts to refactor the image optimization code, I couldn't fully implement lazy loading with the Astro image component. It seems there was a specific reason for not including this feature in the original theme. Other than that, once I learned the file structure, the rest of the development was smooth.
Post-Launch Reflections
Since launching, we’re still in the process of replacing placeholder content with final versions. We haven't had much interaction from users yet, but it’s early days. The website was easy to deploy and has already achieved its core goals—establishing an online presence for my wife’s freelance business. We’re optimistic that it will attract new clients and make it easier for her existing network to stay in touch.