
When Nicholas and I first started talking about LoveVisuals, two things were clear: the site needed to be discoverable, and it needed to handle a lot of video content without sacrificing performance. This post dives into how we tackled both challenges.
The SEO Challenge
LoveVisuals isn’t a blog or a news site. It’s a portfolio, and that means the content is visual, the structure is custom, and the stakes are high. Nicholas wanted his work to be discoverable by potential clients, collaborators, and anyone searching for top-tier video production in Brooklyn (or beyond).
Automating SEO at Scale
I built a system of reusable components to handle SEO at scale. The heart of it is MetaDescription.astro
, which takes in props like title
, description
, and image
, and spits out a full suite of meta tags for search engines and social platforms.
Every page—whether it’s a project, a brand, or the homepage—gets:
- A unique, canonical URL
- Open Graph and Twitter Card tags for rich social previews
- Schema.org structured data for Google’s rich results
Here’s a taste of what that looks like in code:
<!-- MetaDescription.astro (snippet) -->
<meta name="title" content={title} />
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content={type} />
<meta property="og:url" content={canonicalURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={canonicalURL} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={image} />
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": type === "article" ? "Article" : "WebPage",
"headline": title,
"description": description,
"image": image,
"url": canonicalURL,
"publisher": {
"@type": "Organization",
"name": "Love Visuals",
"logo": {
"@type": "ImageObject",
"url": "/assets/images/logo.png"
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": canonicalURL
}
}
</script>
Sitemaps and Discoverability
A big part of SEO is making sure search engines can actually find your content. For LoveVisuals, I automated sitemap generation so every project and brand page is included. As Nicholas adds new work, the sitemap updates—no manual intervention required.
The Video Challenge
If there was a single page that kept me up at night, it was the brands page. Nicholas wanted to showcase over 30 videos—each one a testament to his work, each one a potential performance nightmare.
Lazy Loading with Intersection Observer
Instead of loading all 30+ iframes on page load, I used Svelte’s lifecycle and the browser’s Intersection Observer API to only load a video when it was about to enter the viewport.
let isVisible = false;
let observer: IntersectionObserver | null = null;
onMount(() => {
observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
isVisible = true;
observer?.unobserve(entry.target);
}
});
}, { threshold: 0.1, rootMargin: "100px" });
if (container) observer.observe(container);
return () => observer?.disconnect();
});
Optimized Embed URLs
I didn’t just use the default YouTube and Vimeo links. I generated them with parameters for:
- Lower video quality on initial load (
vq=medium
for YouTube,quality=540p
for Vimeo) - Privacy (
dnt=1
for Vimeo,modestbranding=1
for YouTube) - Lazy loading (
loading=lazy
on the iframe itself)
export const generateYoutubeEmbed = (videoID: string) =>
`https://www.youtube.com/embed/${videoID}?loading=lazy&vq=medium`;
export const generateVimeoEmbed = (videoID: string) =>
`https://player.vimeo.com/video/${videoID}?h=25719eb646&title=0&portrait=0`;
Custom Placeholders and Error Handling
Before the iframe loads, users see a branded play button and (for YouTube) a thumbnail. If a video fails to load, the component shows a clear error message. It’s a small thing, but it’s better than a broken or blank embed.
Accessibility
I made sure the play button is keyboard-accessible and the iframe has a descriptive title. This isn’t just for screen readers—it’s for anyone who navigates with a keyboard.
Performance Testing
After implementing these optimizations, I ran the site through Lighthouse and WebPageTest. The results were impressive:
- Initial page load was fast, even with dozens of videos
- Lighthouse scores were consistently in the high 90s
- Social previews looked great across platforms
- The site was fully accessible and SEO-friendly
What Worked, What Didn’t
-
What worked:
- Automated SEO components made it easy to maintain consistency
- Lazy loading and optimized URLs made the video page performant
- The combination of Astro and Svelte was perfect for this use case
-
What didn’t:
- Vimeo thumbnails require an API call
- Tracking video plays is trickier with lazy loading
- Some edge cases required manual overrides in the SEO components
What I learned:
Building a high-performance portfolio site is about more than just code—it’s about finding the right balance between discoverability, performance, and user experience. The combination of Astro’s static site generation and Svelte’s component islands made it possible to build something that’s both beautiful and fast.
This is Part 2 of the LoveVisuals series. You can read the other posts here: