A modern, responsive personal portfolio website built with Astro 5, TailwindCSS, and TypeScript. Features dark mode support, dynamic social media links, and a clean, minimalist design with optimized performance.
- π¨ Responsive Design: Mobile-first, clean UI with TailwindCSS
- π Dark/Light Mode: System preference + manual toggle without flashes
- π Dynamic Social Links: Centralized in
src/config/socials.ts - πΌοΈ Images via astro:assets:
ResponsiveImage.astrowrapsPicturefor AVIF/WebP + original fallback- SVG/string paths fall back to
<img>automatically - Homepage avatar now uses
Picturewith responsive variants
- π§© MDX Support: Write content in
.mdx, import images, and embed components - π Mermaid Diagrams: Enabled using
rehype-mermaidin Markdown - β‘ Performance: Minimal JS, code-splitting, responsive images
- π¦ Caching & Headers: Netlify headers auto-generated after build
- π Type Safety: TypeScript across content & components
graph TD
A[Layout.astro] --> B[index.astro]
B --> C[Social Links]
B --> D[Theme Toggle]
C --> E[socials.ts]
D --> F[Theme Management]
F --> G[System Preference]
F --> H[Manual Toggle]
F --> I[LocalStorage]
style A fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#bbf,stroke:#333,stroke-width:2px
style E fill:#dfd,stroke:#333,stroke-width:2px
/
βββ public/
β βββ images/
β βββ fonts/
β βββ favicon.svg
βββ scripts/
β βββ optimize-images.js # prebuild image optimization
β βββ generate-headers.js # postbuild Netlify _headers
β βββ fix-fonts.js
βββ src/
β βββ components/
β β βββ ResponsiveImage.astro
β β βββ FontOptimizer.astro
β β βββ SocialIcons.astro
β β βββ Icons.astro
β βββ content/
β β βββ blog/ # .md or .mdx files
β β βββ projects/ # .md or .mdx files
β β βββ config.ts # collections schema
β βββ images/
β β βββ avatar.jpg # homepage avatar (astro:assets)
β βββ layouts/
β β βββ Layout.astro
β β βββ BlogPost.astro
β β βββ ProjectPost.astro
β βββ pages/
β β βββ index.astro
β β βββ blog.astro
β β βββ blog/[...slug].astro
β β βββ projects.astro
β β βββ projects/[...slug].astro
β β βββ tags/[tag].astro
βββ astro.config.mjs
βββ netlify.toml
βββ tailwind.config.mjs
βββ package.json
βββ README.md
-
Clone the repository
git clone https://github.com/yourusername/portfolio-website.git cd portfolio-website -
Install dependencies
npm install
-
Configure social media links Edit
src/config/socials.ts:export const socials: Social[] = [ { platform: 'github', url: 'https://github.com/yourusername', label: 'GitHub', }, // Add more social links... ];
-
Start development server
npm run dev
This site uses PostHog for privacy-friendly web analytics.
- Location:
src/components/posthog.astrois included once in the base layout (src/layouts/Layout.astro). - Client env vars: Only
PUBLIC_*variables are exposed to the browser. We use:PUBLIC_POSTHOG_KEY: your PostHog Project API keyPUBLIC_POSTHOG_HOST: API host (defaulthttps://us.i.posthog.com, EU:https://eu.i.posthog.com)
- Env files:
.env.public(committed): holds safe public values. Used for both dev and production builds..env.example(committed): template with placeholders for new environments..env(gitignored): created automatically from.env.publicif missing.
- Loading mechanism:
- A small script (
scripts/sync-public-env.js) runs before dev/start/build to copy.env.publicβ.envif.envis missing so Vite/Astro load the values. - You can also set
PUBLIC_POSTHOG_*directly in your hosting providerβs environment variables.
- A small script (
Quick start:
- Set your key in
.env.public(or host env):PUBLIC_POSTHOG_KEY=phc_XXXXXXXXXXXXXXXXXXXXXXXX PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
- Run
npm run devand open the site. Events are enabled in dev for easy testing. - Deploy and set the same env variables in your hosting UI.
Notes:
- The key is intentionally public; PostHog client keys are safe to expose.
- If you want to filter dev vs prod traffic in PostHog, you can register an env tag after init:
posthog.register({ env: import.meta.env.MODE })
- High-level wrapper over
astro:assetsPicture - Inputs
ImageMetadata | string; auto-falls back to<img>for SVG/string - Responsive widths default:
[320, 480, 768, 1024, 1280] - Formats default:
['avif', 'webp']plus original format fallback - Props:
sizes?,widths?,formats?,loading?,fetchpriority?
- Font loading optimization
- Variable font support
- Loading state management
- Font fallback handling
- Performance monitoring
- Third-party script management
- Priority-based loading
- Resource hint implementation
- Performance budgeting
- User interaction tracking
- Base template for all pages
- Implements navigation and theme toggle
- Handles dark mode functionality with optimized JavaScript
- Responsive design implementation
- Main landing page with optimized avatar via
Picture(AVIF/WebP + responsive) - Dynamic social links with inline SVG icons
- Responsive layout with dark mode
- Optimized SVG icons instead of external icon libraries
- Zero external dependencies for icons
- TypeScript-powered type safety for icon names
- Customizable through CSS classes
- Accessible and performant
- TypeScript interface for social media entries
- Centralized configuration for all social links
- SVG icon integration
- Easy to extend and modify
- Optimized theme switching with minimal JavaScript
- System preference detection
- Manual theme toggle
- Persistent theme selection using localStorage
- Prevents flash of incorrect theme
- WebP format support with JPEG fallback
- Multiple sizes for responsive images (1x and 2x)
- Proper width and height attributes to prevent layout shifts
- Lazy loading for off-screen images
- Sharp-powered image processing
- Automated optimization script
Content lives in src/content/blog/ and src/content/projects/, defined by src/content/config.ts with schema-validated frontmatter using image().
-
Frontmatter schema (
src/content/config.ts):- Blog:
title(string),date(string),heroImage(image optional),tags?,description? - Projects:
title,description,date,heroImage?,tags?,github?,demo?
- Blog:
-
Create a new blog post
- Create a folder with media next to it (recommended):
src/content/blog/my-post/index.mdxsrc/content/blog/my-post/hero.jpg
- In
index.mdx:--- title: My Post date: 2025-09-02 heroImage: ./hero.jpg tags: [astro] --- import { Picture } from 'astro:assets'; import ResponsiveImage from '../../components/ResponsiveImage.astro'; import diagram from './diagram.png'; Inline image via Picture: <Picture src={diagram} widths={[320,640,960,1280]} sizes="(min-width:768px) 768px, 100vw" formats={['avif','webp','png']} alt="Diagram" /> Or via shared component: <ResponsiveImage src={diagram} alt="Diagram" sizes="(min-width:768px) 768px, 100vw" />
- Create a folder with media next to it (recommended):
-
Create a new project
src/content/projects/my-project/index.mdx- Frontmatter:
--- title: My Project description: Short summary date: 2025-09-02 heroImage: ./hero.png github: https://github.com/you/repo demo: https://example.com ---
-
Hero images
- Store images next to the content file, reference with a relative path.
- When schema
image()resolves toImageMetadata, the routes/layouts render withResponsiveImage.astro(usesPictureunder the hood). SVGs fall back to<img>.
-
Routing
- Blog pages:
src/pages/blog/[...slug].astro(usesgetCollection('blog')andpost.render()). - Project pages:
src/pages/projects/[...slug].astro.
- Blog pages:
-
Mermaid diagrams
- Markdown/MDX diagrams are rendered server-side via
rehype-mermaidconfigured inastro.config.mjs.
- Markdown/MDX diagrams are rendered server-side via
# Optimize images
node scripts/optimize-images.js
# Analyze bundle
npm run analyze
# Build with optimizations
npm run build- Integrated with Core Web Vitals
- Real User Monitoring (RUM)
- Performance budget tracking
- Third-party impact monitoring
# View optimization metrics
npm run analyze
# Check bundle sizes
npm run build -- --debugImplemented via generated Netlify headers and hashed filenames:
- Hashed assets:
/assets/*immutable for 1 year - Bundled JS:
/chunks/*.jsand/entry.*.jsimmutable for 1 year - Fonts: woff2 immutable for 1 year
- HTML:
max-age=0, must-revalidate
How it works:
scripts/generate-headers.jswritesdist/_headersinpostbuild- Filenames include content hashes per
astro.config.mjsrollup output settings
Modify headers:
- Edit
scripts/generate-headers.js, rebuild. The old root_headersfile was removed in favor of generated headers.
The website is designed to prevent common browser console errors:
- Progressive Loading: Scripts are loaded in priority tiers (high, medium, low) to optimize page performance
- Proper Variable Scoping: All script variables are properly defined in their execution context
- Error Handling: Script loading includes proper error handling to prevent reference errors
- Cookie Consent Integration: Third-party scripts are only loaded after obtaining user consent
- Correct Font Weight Syntax: Using numeric weights (e.g.,
700) instead of descriptive weights (e.g.,bold) - Font Loading Error Handling: Graceful fallback to system fonts if web fonts fail to load
- Font Loading State Management: Proper tracking of font loading state in localStorage
- Proper CSS Property Syntax: Ensuring all CSS properties include required semicolons
- Valid CSS Values: Using valid CSS values for all properties including aspect-ratio
- Cookie Consent Banner: User-friendly banner for obtaining cookie consent
- Privacy Policy Page: Detailed privacy policy explaining data collection practices
- SameSite Cookie Handling: Proper handling of third-party cookies with SameSite attributes
- Conditional Script Loading: Third-party scripts only load when consent is given
- Open
src/config/socials.ts - Add a new entry to the
socialsarray:{ platform: 'newplatform', url: 'https://newplatform.com/username', label: 'Platform Name' }
- Open
src/config/icons.ts - Add your SVG path:
export const Icons = { NewIcon: `<path d="..." />`, };
<Icons name="NewIcon" class="h-6 w-6" />- Open
tailwind.config.mjs - Customize the theme section:
theme: { extend: { colors: { // Add your custom colors } } }
| Command | Action |
|---|---|
npm install |
Installs dependencies |
npm run dev |
Starts local dev server at localhost:4321 |
npm run build |
Build your production site to ./dist/ |
npm run preview |
Preview your build locally before deploying |
node scripts/optimize-images.js |
Optimize and convert images |
- Hosted on Netlify. Build command:
npm run build; publish directory:dist/. - Postbuild generates
dist/_headersfor caching (scripts/generate-headers.js). - For local preview:
npm run preview.
- LCP (Largest Contentful Paint): < 2.5s
- FID (First Input Delay): < 100ms
- CLS (Cumulative Layout Shift): < 0.1
- TTFB: Optimized server response
- FCP: Fast first content paint
- TTI: Quick time to interactive
- Images:
- WebP format with JPEG fallbacks
- Responsive sizes (1x and 2x)
- Proper width/height attributes
- Lazy loading implementation
- JavaScript:
- Minimal usage
- Code splitting
- Async loading where possible
- Optimized theme switching
- Icons:
- Inline SVGs instead of icon fonts
- No external icon libraries
- CSS-based styling
- CSS:
- Purged unused styles
- Minimal Tailwind imports
- Efficient dark mode implementation
- Caching:
- Proper cache headers for static assets
- Local storage for user preferences
- Core:
astro,@astrojs/tailwind,sharp,rehype-mermaid,date-fns - Dev:
@astrojs/mdx, ESLint/Prettier toolchain, TailwindCSS - See
package.jsonfor exact versions.
- Astro: Static site generator
- TailwindCSS: Utility-first CSS framework
- TypeScript: Type safety
- Sharp: Image optimization
import mdx from '@astrojs/mdx';
import tailwind from '@astrojs/tailwind';
export default defineConfig({
prefetch: true,
integrations: [tailwind(), mdx()],
build: {
assets: 'assets',
rollupOptions: {
output: {
entryFileNames: 'entry.[hash].js',
chunkFileNames: 'chunks/[name].[hash].js',
assetFileNames: 'assets/[name].[hash][extname]'
}
}
},
markdown: {
rehypePlugins: [require('rehype-mermaid')]
}
});// scripts/optimize-images.js
module.exports = {
quality: 80,
formats: ['webp', 'avif'],
sizes: [640, 768, 1024, 1280],
};- Modern browsers (Chrome, Firefox, Safari, Edge)
- Responsive design works on all screen sizes
- Progressive enhancement approach
- Fallbacks for older browsers
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
This project is open source and available under the MIT License.