Update site configuration and enhance SEO with structured data
- Changed site URL in configuration from 'https://example.com' to 'https://nicholai.work'. - Updated site title and description for better SEO optimization. - Removed outdated portrait images and replaced them with optimized formats in the hero section. - Enhanced BaseHead component with structured data for improved search engine visibility. - Added reading time calculation to blog posts for better user engagement.
This commit is contained in:
parent
e2d2ada330
commit
673a937db3
@ -12,7 +12,7 @@ import react from '@astrojs/react';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://example.com',
|
||||
site: 'https://nicholai.work',
|
||||
integrations: [mdx(), sitemap(), react()],
|
||||
|
||||
adapter: cloudflare({
|
||||
|
||||
30
dev/blog_template.mdx
Normal file
30
dev/blog_template.mdx
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
title: 'Your Blog Post Title Here'
|
||||
description: 'A compelling one-sentence description that summarizes the blog post. This appears in meta tags and previews.'
|
||||
pubDate: 'Jan 1 2024'
|
||||
heroImage: '../../assets/blog-placeholder-1.jpg'
|
||||
featured: true
|
||||
category: 'Case Study'
|
||||
tags: ['VFX', 'Houdini', 'Nuke', 'AI/ML', 'Brand Film']
|
||||
---
|
||||
|
||||
<!-- Introduction paragraph: Set the context and hook the reader -->
|
||||
[Write your opening paragraph here. Introduce the project, collaboration, or topic. This should be engaging and provide context for what follows.]
|
||||
|
||||
<!-- Optional: Main hero video or image -->
|
||||
<div class="video-container my-10">
|
||||
<video controls class="w-full border border-white/10">
|
||||
<source src="https://media.nicholai.work/your-video.mp4" type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<p class="text-slate-500 text-sm mt-3 font-mono">/// FINAL FILM</p>
|
||||
</div>
|
||||
|
||||
<!-- Alternative: If using an image instead of video -->
|
||||
<!--
|
||||
<div class="my-10">
|
||||
<img src="path/to/image.jpg" alt="Description" class="w-full border border-white/10" />
|
||||
<p class="text-slate-500 text-sm mt-3 font-mono">/// HERO IMAGE</p>
|
||||
</div>
|
||||
-->
|
||||
|
||||
5
public/robots.txt
Normal file
5
public/robots.txt
Normal file
@ -0,0 +1,5 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://nicholai.work/sitemap-index.xml
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 3.3 MiB |
|
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 199 KiB |
@ -3,23 +3,110 @@
|
||||
// all pages through the use of the <BaseHead /> component.
|
||||
import '../styles/global.css';
|
||||
import type { ImageMetadata } from 'astro';
|
||||
import FallbackImage from '../assets/blog-placeholder-1.jpg';
|
||||
import { SITE_TITLE } from '../consts';
|
||||
import DefaultOGImage from '../assets/nicholai-medium-portrait.jpg';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION, SOCIAL_LINKS } from '../consts';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
image?: ImageMetadata;
|
||||
type?: 'website' | 'article';
|
||||
publishedTime?: Date;
|
||||
modifiedTime?: Date;
|
||||
}
|
||||
|
||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
|
||||
const { title, description, image = FallbackImage } = Astro.props;
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
image = DefaultOGImage,
|
||||
type = 'website',
|
||||
publishedTime,
|
||||
modifiedTime,
|
||||
} = Astro.props;
|
||||
|
||||
// Structured Data - Person Schema (optimized for rich results)
|
||||
const personSchema = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Person",
|
||||
"@id": "https://nicholai.work/#person",
|
||||
"name": "Nicholai Vogel",
|
||||
"givenName": "Nicholai",
|
||||
"familyName": "Vogel",
|
||||
"url": "https://nicholai.work",
|
||||
"email": SOCIAL_LINKS.email,
|
||||
"image": new URL(DefaultOGImage.src, Astro.site).toString(),
|
||||
"jobTitle": "VFX Supervisor",
|
||||
"description": "VFX Supervisor and Houdini Artist specializing in commercial and music video visual effects",
|
||||
"worksFor": {
|
||||
"@type": "Organization",
|
||||
"name": "Biohazard VFX",
|
||||
"url": "https://biohazardvfx.com"
|
||||
},
|
||||
"knowsAbout": [
|
||||
"Visual Effects",
|
||||
"VFX Supervision",
|
||||
"Compositing",
|
||||
"Houdini",
|
||||
"SideFX Houdini",
|
||||
"Nuke",
|
||||
"The Foundry Nuke",
|
||||
"3D Animation",
|
||||
"AI/ML Integration",
|
||||
"Motion Graphics",
|
||||
"Commercial VFX",
|
||||
"Music Video VFX"
|
||||
],
|
||||
"sameAs": [
|
||||
"https://instagram.com/nicholai.exe/",
|
||||
SOCIAL_LINKS.linkedin,
|
||||
"https://biohazardvfx.com"
|
||||
],
|
||||
"alumniOf": [],
|
||||
"award": []
|
||||
};
|
||||
|
||||
// Structured Data - WebSite Schema with potential search action
|
||||
const websiteSchema = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"@id": "https://nicholai.work/#website",
|
||||
"name": SITE_TITLE,
|
||||
"description": SITE_DESCRIPTION,
|
||||
"url": "https://nicholai.work",
|
||||
"inLanguage": "en-US",
|
||||
"author": {
|
||||
"@id": "https://nicholai.work/#person"
|
||||
},
|
||||
"publisher": {
|
||||
"@id": "https://nicholai.work/#person"
|
||||
}
|
||||
};
|
||||
|
||||
// Structured Data - Professional Service (helps with local/service searches)
|
||||
const professionalServiceSchema = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ProfessionalService",
|
||||
"@id": "https://nicholai.work/#service",
|
||||
"name": "Nicholai Vogel - VFX Services",
|
||||
"description": "Professional visual effects services including VFX supervision, Houdini FX, compositing, and AI integration for commercials and music videos",
|
||||
"url": "https://nicholai.work",
|
||||
"provider": {
|
||||
"@id": "https://nicholai.work/#person"
|
||||
},
|
||||
"areaServed": "Worldwide",
|
||||
"serviceType": ["VFX Supervision", "Visual Effects", "Compositing", "3D Animation", "Motion Graphics"]
|
||||
};
|
||||
---
|
||||
|
||||
<!-- Global Metadata -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="theme-color" content="#0B0D11" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta name="author" content="Nicholai Vogel" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/favicon-192.png" />
|
||||
@ -33,15 +120,24 @@ const { title, description, image = FallbackImage } = Astro.props;
|
||||
/>
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<!-- Fonts - Preconnect and load with display=swap -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=Space+Mono:wght@400;700&display=swap"
|
||||
rel="stylesheet">
|
||||
rel="stylesheet"
|
||||
media="print"
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=Space+Mono:wght@400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</noscript>
|
||||
|
||||
<!-- Icons -->
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<!-- Icons - Load async to prevent render blocking -->
|
||||
<script src="https://unpkg.com/lucide@latest" defer></script>
|
||||
|
||||
<!-- Canonical URL -->
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
@ -52,15 +148,25 @@ const { title, description, image = FallbackImage } = Astro.props;
|
||||
<meta name="description" content={description} />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:type" content={type} />
|
||||
<meta property="og:url" content={Astro.url} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={new URL(image.src, Astro.url)} />
|
||||
<meta property="og:site_name" content={SITE_TITLE} />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
{publishedTime && <meta property="article:published_time" content={publishedTime.toISOString()} />}
|
||||
{modifiedTime && <meta property="article:modified_time" content={modifiedTime.toISOString()} />}
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content={Astro.url} />
|
||||
<meta property="twitter:title" content={title} />
|
||||
<meta property="twitter:description" content={description} />
|
||||
<meta property="twitter:image" content={new URL(image.src, Astro.url)} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content={Astro.url.toString()} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={new URL(image.src, Astro.url).toString()} />
|
||||
<meta name="twitter:creator" content="@nicholai_exe" />
|
||||
|
||||
<!-- Structured Data - JSON-LD -->
|
||||
<script type="application/ld+json" set:html={JSON.stringify(personSchema)} />
|
||||
<script type="application/ld+json" set:html={JSON.stringify(websiteSchema)} />
|
||||
<script type="application/ld+json" set:html={JSON.stringify(professionalServiceSchema)} />
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
---
|
||||
import { Picture } from 'astro:assets';
|
||||
import heroPortrait from '../../assets/nicholai-closeup-portrait.JPEG';
|
||||
|
||||
interface Props {
|
||||
headlineLine1: string;
|
||||
headlineLine2: string;
|
||||
@ -11,14 +14,19 @@ interface Props {
|
||||
const { headlineLine1, headlineLine2, portfolioYear, location, locationLabel, bio } = Astro.props;
|
||||
---
|
||||
<section id="hero" class="relative w-full h-[100dvh] overflow-hidden bg-brand-dark">
|
||||
<!-- Background Image (Portrait) -->
|
||||
<!-- Background Image (Portrait) - Optimized with AVIF/WebP -->
|
||||
<div class="absolute top-0 right-0 w-full md:w-1/2 h-full z-0">
|
||||
<div class="relative w-full h-full">
|
||||
<img
|
||||
src="/media/nicholai-closeup-portrait.JPEG"
|
||||
alt="Nicholai Portrait"
|
||||
<Picture
|
||||
src={heroPortrait}
|
||||
formats={['avif', 'webp']}
|
||||
widths={[640, 1024, 1600]}
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
alt="Nicholai Vogel portrait"
|
||||
class="w-full h-full object-cover object-center opacity-0 mix-blend-luminosity md:opacity-0 transition-opacity duration-[2000ms] ease-out delay-500 intro-element"
|
||||
id="hero-portrait"
|
||||
loading="eager"
|
||||
decoding="sync"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-gradient-to-l from-transparent via-brand-dark/50 to-brand-dark"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-brand-dark via-transparent to-transparent"></div>
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
// Place any global data in this file.
|
||||
// You can import this data from anywhere in your site by using the `import` keyword.
|
||||
|
||||
export const SITE_TITLE = 'Nicholai Vogel | VFX Artist & Technical Generalist';
|
||||
export const SITE_DESCRIPTION = 'VFX Artist & Technical Generalist specializing in Houdini, Nuke, and AI/ML integration. Founder of Biohazard VFX.';
|
||||
// SEO-optimized title (under 60 characters for full display in search results)
|
||||
export const SITE_TITLE = 'Nicholai Vogel — VFX Supervisor & Technical VFX Artist';
|
||||
|
||||
// SEO-optimized description (under 160 characters, includes keywords and CTA)
|
||||
export const SITE_DESCRIPTION = 'VFX Supervisor specializing in both 2D and 3D VFX, AI and highend technical visualization. Clients: G-Star Raw, Interscope, Ralph Lauren. Founder of Biohazard VFX.';
|
||||
|
||||
export const SOCIAL_LINKS = {
|
||||
email: 'nicholai@nicholai.work',
|
||||
website: 'https://nicholai.work',
|
||||
linkedin: '#' // Update when available
|
||||
linkedin: 'https://www.linkedin.com/in/nicholai-vogel-7a6b85112/'
|
||||
};
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
---
|
||||
import type { ImageMetadata } from 'astro';
|
||||
import BaseHead from '../components/BaseHead.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import GridOverlay from '../components/GridOverlay.astro';
|
||||
@ -10,15 +11,35 @@ interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
usePadding?: boolean;
|
||||
image?: ImageMetadata;
|
||||
type?: 'website' | 'article';
|
||||
publishedTime?: Date;
|
||||
modifiedTime?: Date;
|
||||
}
|
||||
|
||||
const { title = SITE_TITLE, description = SITE_DESCRIPTION, usePadding = true } = Astro.props;
|
||||
const {
|
||||
title = SITE_TITLE,
|
||||
description = SITE_DESCRIPTION,
|
||||
usePadding = true,
|
||||
image,
|
||||
type = 'website',
|
||||
publishedTime,
|
||||
modifiedTime,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="scroll-smooth">
|
||||
<head>
|
||||
<BaseHead title={title} description={description} />
|
||||
<BaseHead
|
||||
title={title}
|
||||
description={description}
|
||||
image={image}
|
||||
type={type}
|
||||
publishedTime={publishedTime}
|
||||
modifiedTime={modifiedTime}
|
||||
/>
|
||||
<slot name="head" />
|
||||
</head>
|
||||
<body class="antialiased selection:bg-brand-accent selection:text-brand-dark bg-brand-dark text-white">
|
||||
<CustomCursor client:load />
|
||||
|
||||
@ -37,6 +37,7 @@ interface Props {
|
||||
prevPost?: NavPost;
|
||||
nextPost?: NavPost;
|
||||
relatedPosts?: RelatedPost[];
|
||||
readTime?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
@ -51,13 +52,47 @@ const {
|
||||
prevPost,
|
||||
nextPost,
|
||||
relatedPosts = [],
|
||||
readTime = '5 min read',
|
||||
} = Astro.props;
|
||||
|
||||
// Estimated read time (rough calculation)
|
||||
const readTime = '5 min read';
|
||||
// Article structured data (JSON-LD)
|
||||
const articleSchema = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": title,
|
||||
"description": description,
|
||||
"datePublished": pubDate.toISOString(),
|
||||
"dateModified": (updatedDate || pubDate).toISOString(),
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Nicholai Vogel",
|
||||
"url": "https://nicholai.work"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Person",
|
||||
"name": "Nicholai Vogel",
|
||||
"url": "https://nicholai.work"
|
||||
},
|
||||
"mainEntityOfPage": {
|
||||
"@type": "WebPage",
|
||||
"@id": Astro.url.href
|
||||
},
|
||||
...(heroImage && { "image": new URL(heroImage.src, Astro.url).toString() }),
|
||||
...(category && { "articleSection": category }),
|
||||
...(tags && tags.length > 0 && { "keywords": tags.join(", ") })
|
||||
};
|
||||
---
|
||||
|
||||
<BaseLayout title={title} description={description} image={heroImage}>
|
||||
<BaseLayout
|
||||
title={title}
|
||||
description={description}
|
||||
image={heroImage}
|
||||
type="article"
|
||||
publishedTime={pubDate}
|
||||
modifiedTime={updatedDate}
|
||||
>
|
||||
<!-- Article Structured Data -->
|
||||
<script type="application/ld+json" set:html={JSON.stringify(articleSchema)} slot="head" />
|
||||
<ReadingProgress />
|
||||
|
||||
<article class="relative pb-24">
|
||||
|
||||
@ -89,6 +89,12 @@ interface Props {
|
||||
|
||||
const { post, prevPost, nextPost, relatedPosts } = Astro.props;
|
||||
const { Content, headings } = await render(post);
|
||||
|
||||
// Calculate reading time (average 200 words per minute)
|
||||
const wordsPerMinute = 200;
|
||||
const wordCount = post.body?.split(/\s+/).length || 0;
|
||||
const readingTime = Math.max(1, Math.ceil(wordCount / wordsPerMinute));
|
||||
const readTimeText = `${readingTime} min read`;
|
||||
---
|
||||
|
||||
<BlogPost
|
||||
@ -97,6 +103,7 @@ const { Content, headings } = await render(post);
|
||||
prevPost={prevPost}
|
||||
nextPost={nextPost}
|
||||
relatedPosts={relatedPosts}
|
||||
readTime={readTimeText}
|
||||
>
|
||||
<Content />
|
||||
</BlogPost>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user