Website Redesign Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Redesign shreyasgokhale.com from a resume-centric single page into a hub-style personal website with route-based sections, minimalist “black ink on white paper” design, dark mode, and print-optimized resume.
Architecture: Astro 4 static site with content collections sourced via symlink from an Obsidian vault. Pages built as .astro files, styled with Tailwind CSS + typography plugin. Existing components are refactored (not rewritten from scratch) where possible. New routes added for landing hub, resume, blog sections, now, contact, and tags.
Tech Stack: Astro 4, Tailwind CSS 3, TypeScript, MDX, Pagefind, @fontsource/inter, @fontsource/lora
File Structure
Files to Create
| File | Responsibility |
|---|---|
src/pages/index.astro | Hub landing page with intro + card grid |
src/pages/resume/index.astro | Full resume page (printable) |
src/pages/resume/[...slug].astro | Experience detail page |
src/pages/scribbles/index.astro | Blog index (personal writing) |
src/pages/scribbles/[...slug].astro | Individual blog post |
src/pages/subroutines/index.astro | Technical writeups index |
src/pages/subroutines/[...slug].astro | Individual technical post |
src/pages/gsoc/index.astro | GSoC 2020 index |
src/pages/gsoc/[...slug].astro | Individual GSoC post |
src/pages/faq/index.astro | Masters FAQ page |
src/pages/now.astro | Now page |
src/pages/contact.astro | Contact form page |
src/pages/tags/index.astro | All tags with counts |
src/pages/tags/[tag].astro | Content filtered by tag |
src/pages/domain.astro | Domain map placeholder |
src/components/Nav.astro | New top navigation bar |
src/components/Card.astro | Landing page hub card |
src/components/Tag.astro | Pill-shaped tag with accent color |
src/components/ExperienceEntry.astro | Work/education entry for resume |
src/components/PostList.astro | Post listing for blog indexes |
src/components/Prose.astro | Markdown content wrapper |
src/components/ThemeToggle.astro | Sun/moon theme toggle |
src/components/MobileMenu.astro | Hamburger menu for mobile nav |
src/lib/tags.ts | Tag color mapping and utilities |
Files to Modify
| File | Changes |
|---|---|
src/styles/global.css | New base styles, print stylesheet, section CSS vars |
src/consts.ts | New route metadata, social links, hub card definitions |
src/types.ts | New types for cards, tags, nav items |
src/layouts/PageLayout.astro | Use new Nav/Footer, accept section prop |
src/components/Footer.astro | Redesign: social icons + copyright, move theme toggle to nav |
src/components/Head.astro | Remove Devicon CDN, update site metadata |
tailwind.config.mjs | Add domain accent colors |
astro.config.mjs | Update site URL |
content/green/Resume/content/config.ts | Add tags and draft to work/education schemas; add scribbles, subroutines, gsoc, now collections |
Files to Delete
| File | Reason |
|---|---|
src/pages/work/index.astro | Replaced by /resume |
src/pages/work/[...slug].astro | Replaced by /resume/[...slug] |
src/pages/education/index.astro | Merged into /resume |
src/pages/projects/index.astro | Merged into /resume |
src/components/HomeContainer.astro | Replaced by new landing layout |
src/components/BackToTop.astro | Removed per minimalist design |
src/components/BackToPrev.astro | Replaced by inline back links |
src/lib/skillIcons.ts | Devicon CDN removed; skills become Tag pills |
Task 1: Create Feature Branch and Set Up Foundation
Files:
-
Modify:
tailwind.config.mjs -
Modify:
src/consts.ts -
Modify:
src/types.ts -
Create:
src/lib/tags.ts -
Step 1: Create and switch to the redesign branch
git checkout -b feat/website-redesign- Step 2: Add domain accent colors to Tailwind config
Replace the contents of tailwind.config.mjs:
import defaultTheme from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: [
"./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}",
],
theme: {
extend: {
fontFamily: {
sans: ["Inter", ...defaultTheme.fontFamily.sans],
serif: ["Lora", ...defaultTheme.fontFamily.serif],
},
colors: {
accent: {
amber: {
light: "#f59e0b",
dark: "#fbbf24",
bg: "#fef3c7",
"bg-dark": "#78350f",
},
teal: {
light: "#14b8a6",
dark: "#2dd4bf",
bg: "#ccfbf1",
"bg-dark": "#134e4a",
},
blue: {
light: "#3b82f6",
dark: "#60a5fa",
bg: "#dbeafe",
"bg-dark": "#1e3a5f",
},
slate: {
light: "#64748b",
dark: "#94a3b8",
bg: "#f1f5f9",
"bg-dark": "#334155",
},
},
},
},
},
plugins: [require("@tailwindcss/typography")],
};- Step 3: Update types
Replace the contents of src/types.ts:
export type Site = {
NAME: string;
EMAIL: string;
DESCRIPTION: string;
};
export type Metadata = {
TITLE: string;
DESCRIPTION: string;
};
export type Socials = {
NAME: string;
HREF: string;
ICON: string;
}[];
export type HubCard = {
TITLE: string;
DESCRIPTION: string;
HREF: string;
};
export type NavItem = {
TITLE: string;
HREF: string;
};
export type TagDomain = "embedded" | "robotics" | "iot" | "software" | "default";- Step 4: Update consts
Replace the contents of src/consts.ts:
import type { Site, Metadata, Socials, HubCard, NavItem } from "@types";
export const SITE: Site = {
NAME: "Shreyas Gokhale",
EMAIL: "[email protected]",
DESCRIPTION: "Robotics, Embedded and IoT System Engineer",
};
export const NAV_ITEMS: NavItem[] = [
{ TITLE: "Resume", HREF: "/resume" },
{ TITLE: "Scribbles", HREF: "/scribbles" },
{ TITLE: "Now", HREF: "/now" },
{ TITLE: "Contact", HREF: "/contact" },
];
export const HOME: Metadata = {
TITLE: "Home",
DESCRIPTION: "Personal website of Shreyas Gokhale — Robotics, Embedded and IoT System Engineer.",
};
export const RESUME: Metadata = {
TITLE: "Resume",
DESCRIPTION: "Work experience, education, and projects.",
};
export const SCRIBBLES: Metadata = {
TITLE: "Scribbles",
DESCRIPTION: "Analects of travels and thoughts.",
};
export const SUBROUTINES: Metadata = {
TITLE: "Subroutines",
DESCRIPTION: "Technical writeups and deep dives.",
};
export const GSOC: Metadata = {
TITLE: "GSoC 2020",
DESCRIPTION: "Google Summer of Code 2020.",
};
export const FAQ: Metadata = {
TITLE: "Masters FAQ",
DESCRIPTION: "Frequently asked questions about studying in Europe.",
};
export const NOW: Metadata = {
TITLE: "Now",
DESCRIPTION: "What Shreyas is currently working on.",
};
export const CONTACT: Metadata = {
TITLE: "Contact",
DESCRIPTION: "Get in touch with Shreyas.",
};
export const TAGS: Metadata = {
TITLE: "Tags",
DESCRIPTION: "Browse content by topic.",
};
export const HUB_CARDS: HubCard[] = [
{ TITLE: "Resume", DESCRIPTION: "Work experience, education, and projects", HREF: "/resume" },
{ TITLE: "Scribbles", DESCRIPTION: "Travel stories and personal thoughts", HREF: "/scribbles" },
{ TITLE: "Subroutines", DESCRIPTION: "Technical writeups and deep dives", HREF: "/subroutines" },
{ TITLE: "GSoC '20", DESCRIPTION: "Google Summer of Code 2020", HREF: "/gsoc" },
{ TITLE: "Masters FAQ", DESCRIPTION: "Studying in Europe — questions answered", HREF: "/faq" },
{ TITLE: "Digital Garden", DESCRIPTION: "Notes, ideas, and evergreen content", HREF: "https://garden.shreyasgokhale.com" },
{ TITLE: "Domain Map", DESCRIPTION: "Areas of expertise and interest", HREF: "/domain" },
{ TITLE: "Contact", DESCRIPTION: "Get in touch", HREF: "/contact" },
];
export const SOCIALS: Socials = [
{ NAME: "LinkedIn", HREF: "https://www.linkedin.com/in/shreyasgokhale", ICON: "linkedin" },
{ NAME: "GitHub", HREF: "https://github.com/shreyasgokhale", ICON: "github" },
{ NAME: "GitLab", HREF: "https://gitlab.com/shreyasgokhale", ICON: "gitlab" },
{ NAME: "Mastodon", HREF: "https://mastodon.social/@shreyasgokhale", ICON: "mastodon" },
{ NAME: "Instagram", HREF: "https://instagram.com/shreyasgokhale", ICON: "instagram" },
];- Step 5: Create tag utilities
Create src/lib/tags.ts:
import type { TagDomain } from "@types";
const TAG_DOMAIN_MAP: Record<string, TagDomain> = {
// Embedded/Hardware
"zephyr": "embedded", "nrf": "embedded", "stm32": "embedded", "kicad": "embedded",
"pcb": "embedded", "fpga": "embedded", "vhdl": "embedded", "embedded": "embedded",
"firmware": "embedded", "rtos": "embedded", "ble": "embedded", "hardware": "embedded",
"arm": "embedded", "esp32": "embedded", "arduino": "embedded",
// Robotics
"ros": "robotics", "ros2": "robotics", "robotics": "robotics", "gazebo": "robotics",
"slam": "robotics", "navigation": "robotics", "moveit": "robotics",
"computer-vision": "robotics", "opencv": "robotics",
// IoT/Cloud
"iot": "iot", "mqtt": "iot", "aws": "iot", "cloud": "iot", "docker": "iot",
"kubernetes": "iot", "terraform": "iot", "ci-cd": "iot", "homeassistant": "iot",
// Software/Systems
"python": "software", "c++": "software", "rust": "software", "typescript": "software",
"linux": "software", "git": "software", "cmake": "software", "testing": "software",
};
export function getTagDomain(tag: string): TagDomain {
return TAG_DOMAIN_MAP[tag.toLowerCase()] ?? "default";
}
export function getTagClasses(tag: string): string {
const domain = getTagDomain(tag);
const styles: Record<TagDomain, string> = {
embedded: "bg-accent-amber-bg text-accent-amber-light dark:bg-accent-amber-bg-dark dark:text-accent-amber-dark",
robotics: "bg-accent-teal-bg text-accent-teal-light dark:bg-accent-teal-bg-dark dark:text-accent-teal-dark",
iot: "bg-accent-blue-bg text-accent-blue-light dark:bg-accent-blue-bg-dark dark:text-accent-blue-dark",
software: "bg-accent-slate-bg text-accent-slate-light dark:bg-accent-slate-bg-dark dark:text-accent-slate-dark",
default: "bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400",
};
return styles[domain];
}- Step 6: Commit foundation
git add tailwind.config.mjs src/types.ts src/consts.ts src/lib/tags.ts
git commit -m "feat: add design system foundation (accent colors, types, consts, tag utils)"Task 2: Core Layout and Navigation
Files:
-
Modify:
src/styles/global.css -
Modify:
src/components/Head.astro -
Create:
src/components/ThemeToggle.astro -
Create:
src/components/Nav.astro -
Create:
src/components/MobileMenu.astro -
Modify:
src/components/Footer.astro -
Modify:
src/layouts/PageLayout.astro -
Step 1: Rewrite global.css
Replace src/styles/global.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
mark {
background-color: yellow;
padding: 0.2em;
border-radius: 0.2em;
}
html {
overflow-y: scroll;
color-scheme: light;
}
html.dark {
color-scheme: dark;
}
html,
body {
@apply size-full;
}
body {
@apply font-sans antialiased;
@apply flex flex-col;
@apply bg-white dark:bg-neutral-950;
@apply text-black dark:text-white;
}
main {
@apply flex-1 py-16 mt-16;
}
/* Prose styling for markdown content */
article {
@apply max-w-full prose dark:prose-invert prose-img:mx-auto prose-img:my-auto;
@apply prose-headings:font-semibold;
@apply prose-headings:text-black prose-headings:dark:text-white;
}
@layer utilities {
article a {
@apply font-sans text-current underline underline-offset-2;
@apply decoration-black/15 dark:decoration-white/30;
@apply transition-colors duration-300 ease-in-out;
}
article a:hover {
@apply text-black dark:text-white;
@apply decoration-black/25 dark:decoration-white/50;
}
}
/* Section personality: scribbles uses serif */
.section-scribbles article {
@apply prose-p:font-serif;
}
/* Section personality: subroutines uses mono accents */
.section-subroutines article code {
@apply font-mono;
}
/* Animation */
.animate {
@apply opacity-0 translate-y-3;
@apply transition-all duration-700 ease-out;
}
.animate.show {
@apply opacity-100 translate-y-0;
}
html #back-to-top {
@apply opacity-0 pointer-events-none;
}
html.scrolled #back-to-top {
@apply opacity-100 pointer-events-auto;
}
/* Print stylesheet for /resume */
@media print {
header,
footer,
nav,
.theme-toggle,
.no-print {
display: none !important;
}
body {
background: white !important;
color: black !important;
font-size: 11pt;
}
main {
padding: 0;
margin: 0;
}
article {
color: black !important;
}
a {
color: black !important;
text-decoration: none !important;
}
.read-more {
display: none !important;
}
.print-tight {
margin-bottom: 0.25rem;
}
h1 { font-size: 18pt; }
h2 { font-size: 14pt; }
h3 { font-size: 12pt; }
}- Step 2: Create ThemeToggle component
Create src/components/ThemeToggle.astro:
---
---
<div class="theme-toggle flex items-center">
<button
id="theme-toggle-btn"
aria-label="Toggle theme"
class="size-8 flex items-center justify-center rounded-full hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
>
<svg
id="sun-icon"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="hidden dark:block"
>
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
<svg
id="moon-icon"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="block dark:hidden"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
</div>
<script is:inline>
function setupThemeToggle() {
const btn = document.getElementById("theme-toggle-btn");
btn?.addEventListener("click", () => {
const isDark = document.documentElement.classList.contains("dark");
if (isDark) {
document.documentElement.classList.remove("dark");
localStorage.setItem("theme", "light");
} else {
document.documentElement.classList.add("dark");
localStorage.setItem("theme", "dark");
}
});
}
document.addEventListener("DOMContentLoaded", setupThemeToggle);
document.addEventListener("astro:after-swap", setupThemeToggle);
</script>- Step 3: Create Nav component
Create src/components/Nav.astro:
---
import { SITE, NAV_ITEMS } from "@consts";
import ThemeToggle from "@components/ThemeToggle.astro";
import MobileMenu from "@components/MobileMenu.astro";
---
<header class="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-neutral-950/80 backdrop-blur-sm border-b border-neutral-200 dark:border-neutral-800">
<div class="max-w-5xl mx-auto px-6 h-16 flex items-center justify-between">
<a href="/" class="font-semibold text-lg hover:opacity-70 transition-opacity">
{SITE.NAME}
</a>
<nav class="hidden md:flex items-center gap-6">
{NAV_ITEMS.map((item) => (
<a
href={item.HREF}
class="text-sm text-neutral-600 dark:text-neutral-400 hover:text-black dark:hover:text-white transition-colors"
>
{item.TITLE}
</a>
))}
<ThemeToggle />
</nav>
<div class="flex items-center gap-2 md:hidden">
<ThemeToggle />
<MobileMenu />
</div>
</div>
</header>- Step 4: Create MobileMenu component
Create src/components/MobileMenu.astro:
---
import { NAV_ITEMS } from "@consts";
---
<button
id="mobile-menu-btn"
aria-label="Open menu"
class="size-8 flex items-center justify-center rounded-full hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
<div id="mobile-menu" class="fixed inset-0 z-50 hidden">
<div class="absolute inset-0 bg-black/20 dark:bg-black/50" id="mobile-menu-overlay"></div>
<div class="absolute right-0 top-0 bottom-0 w-64 bg-white dark:bg-neutral-950 border-l border-neutral-200 dark:border-neutral-800 p-6 pt-20">
<button
id="mobile-menu-close"
aria-label="Close menu"
class="absolute top-5 right-5 size-8 flex items-center justify-center rounded-full hover:bg-neutral-100 dark:hover:bg-neutral-800"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<nav class="flex flex-col gap-4">
{NAV_ITEMS.map((item) => (
<a
href={item.HREF}
class="text-lg text-neutral-600 dark:text-neutral-400 hover:text-black dark:hover:text-white transition-colors"
>
{item.TITLE}
</a>
))}
</nav>
</div>
</div>
<script is:inline>
function setupMobileMenu() {
const btn = document.getElementById("mobile-menu-btn");
const menu = document.getElementById("mobile-menu");
const close = document.getElementById("mobile-menu-close");
const overlay = document.getElementById("mobile-menu-overlay");
function openMenu() { menu?.classList.remove("hidden"); }
function closeMenu() { menu?.classList.add("hidden"); }
btn?.addEventListener("click", openMenu);
close?.addEventListener("click", closeMenu);
overlay?.addEventListener("click", closeMenu);
}
document.addEventListener("DOMContentLoaded", setupMobileMenu);
document.addEventListener("astro:after-swap", setupMobileMenu);
</script>- Step 5: Redesign Footer
Replace src/components/Footer.astro:
---
import { SITE, SOCIALS } from "@consts";
---
<footer class="border-t border-neutral-200 dark:border-neutral-800 py-8">
<div class="max-w-5xl mx-auto px-6 flex flex-col sm:flex-row justify-between items-center gap-4">
<div class="flex items-center gap-4">
{SOCIALS.map((social) => (
<a
href={social.HREF}
target="_blank"
rel="noopener noreferrer"
aria-label={social.NAME}
class="text-neutral-500 hover:text-black dark:hover:text-white transition-colors"
>
<span class="text-sm">{social.NAME}</span>
</a>
))}
</div>
<div class="text-sm text-neutral-500">
© {new Date().getFullYear()} {SITE.NAME}
</div>
</div>
</footer>- Step 6: Update Head component
In src/components/Head.astro, remove the Devicon CDN link (line 62-65) and update the theme preload script to work with the new single-toggle approach. Keep everything else (font preloads, meta tags, ViewTransitions, the font-swap script).
Remove this line:
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/devicons/[email protected]/devicon.min.css"
/>In the init() function inside the inline script, remove the three-button theme listeners (light-theme-button, dark-theme-button, system-theme-button). The theme toggle is now handled by ThemeToggle.astro. Keep preloadTheme, toggleTheme, animate, onScroll, and scrollToTop functions.
- Step 7: Update PageLayout
Replace src/layouts/PageLayout.astro:
---
import Head from "@components/Head.astro";
import Nav from "@components/Nav.astro";
import Footer from "@components/Footer.astro";
import { SITE } from "@consts";
type Props = {
title: string;
description: string;
section?: string;
};
const { title, description, section } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<Head title={`${title} | ${SITE.NAME}`} description={description} />
</head>
<body>
<Nav />
<main class={section ? `section-${section}` : ""}>
<slot />
</main>
<Footer />
</body>
</html>- Step 8: Commit layout and navigation
git add src/styles/global.css src/components/ThemeToggle.astro src/components/Nav.astro src/components/MobileMenu.astro src/components/Footer.astro src/components/Head.astro src/layouts/PageLayout.astro
git commit -m "feat: new layout shell with nav, footer, theme toggle, mobile menu, and print styles"Task 3: Shared Components
Files:
-
Create:
src/components/Tag.astro -
Create:
src/components/Card.astro -
Create:
src/components/ExperienceEntry.astro -
Create:
src/components/PostList.astro -
Create:
src/components/Prose.astro -
Step 1: Create Tag component
Create src/components/Tag.astro:
---
import { getTagClasses } from "@lib/tags";
type Props = {
tag: string;
linked?: boolean;
};
const { tag, linked = true } = Astro.props;
const classes = getTagClasses(tag);
---
{linked ? (
<a
href={`/tags/${tag.toLowerCase()}`}
class={`inline-block px-2 py-0.5 text-xs font-medium rounded-full transition-opacity hover:opacity-80 ${classes}`}
>
{tag}
</a>
) : (
<span class={`inline-block px-2 py-0.5 text-xs font-medium rounded-full ${classes}`}>
{tag}
</span>
)}- Step 2: Create Card component (hub page)
Create src/components/Card.astro:
---
type Props = {
title: string;
description: string;
href: string;
external?: boolean;
};
const { title, description, href, external = false } = Astro.props;
---
<a
href={href}
class="group flex items-center justify-between p-4 rounded-lg border border-neutral-200 dark:border-neutral-800 hover:border-neutral-400 dark:hover:border-neutral-600 transition-colors"
{...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
>
<div>
<div class="font-medium text-black dark:text-white">{title}</div>
<div class="text-sm text-neutral-500 dark:text-neutral-400">{description}</div>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="size-5 flex-shrink-0 stroke-neutral-400 group-hover:stroke-black dark:group-hover:stroke-white stroke-[1.5] fill-none transition-colors ml-4"
>
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</a>- Step 3: Create ExperienceEntry component
Create src/components/ExperienceEntry.astro:
---
import Tag from "@components/Tag.astro";
import { dateRange } from "@lib/utils";
type Props = {
company: string;
role: string;
dateStart: Date;
dateEnd?: Date | string;
location?: string;
tags?: string[];
slug?: string;
collection?: string;
};
const { company, role, dateStart, dateEnd, location, tags = [], slug, collection = "work" } = Astro.props;
---
<div class="py-4">
<div class="text-sm text-neutral-500 print-tight">
{dateRange(dateStart, dateEnd)}
{location && <span> · {location}</span>}
</div>
<div class="font-semibold text-black dark:text-white">{company}</div>
<div class="text-neutral-600 dark:text-neutral-400">{role}</div>
<div class="mt-2">
<slot />
</div>
{tags.length > 0 && (
<div class="flex flex-wrap gap-1 mt-2">
{tags.map((tag) => <Tag tag={tag} />)}
</div>
)}
{slug && (
<a
href={`/resume/${slug}`}
class="read-more inline-block mt-2 text-sm text-neutral-500 hover:text-black dark:hover:text-white transition-colors"
>
Read more →
</a>
)}
</div>- Step 4: Create PostList component
Create src/components/PostList.astro:
---
import Tag from "@components/Tag.astro";
import { formatDate } from "@lib/utils";
type Props = {
posts: {
slug: string;
data: {
title: string;
description?: string;
date: Date;
tags?: string[];
};
collection: string;
}[];
basePath: string;
};
const { posts, basePath } = Astro.props;
---
<ul class="divide-y divide-neutral-200 dark:divide-neutral-800">
{posts.map((post) => (
<li class="py-4">
<div class="text-sm text-neutral-500">{formatDate(post.data.date)}</div>
<a
href={`${basePath}/${post.slug}`}
class="font-medium text-black dark:text-white hover:opacity-70 transition-opacity"
>
{post.data.title}
</a>
{post.data.description && (
<div class="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
{post.data.description}
</div>
)}
{post.data.tags && post.data.tags.length > 0 && (
<div class="flex flex-wrap gap-1 mt-1">
{post.data.tags.map((tag) => <Tag tag={tag} />)}
</div>
)}
</li>
))}
</ul>- Step 5: Create Prose component
Create src/components/Prose.astro:
---
type Props = {
class?: string;
};
const { class: className } = Astro.props;
---
<article class:list={["max-w-none prose dark:prose-invert", className]}>
<slot />
</article>- Step 6: Commit shared components
git add src/components/Tag.astro src/components/Card.astro src/components/ExperienceEntry.astro src/components/PostList.astro src/components/Prose.astro
git commit -m "feat: add shared components (Tag, Card, ExperienceEntry, PostList, Prose)"Task 4: Landing Page (Hub)
Files:
-
Modify:
src/pages/index.astro -
Step 1: Rewrite the landing page
Replace src/pages/index.astro:
---
import PageLayout from "@layouts/PageLayout.astro";
import Card from "@components/Card.astro";
import { HOME, HUB_CARDS } from "@consts";
---
<PageLayout title={HOME.TITLE} description={HOME.DESCRIPTION}>
<div class="max-w-4xl mx-auto px-6">
<section class="mb-12">
<p class="text-lg text-neutral-600 dark:text-neutral-400 max-w-2xl leading-relaxed">
[Greeting and introduction placeholder — 2-3 lines about who Shreyas is,
what he does in Robotics, Embedded, and IoT engineering, and what visitors
can find on this site.]
</p>
</section>
<section>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{HUB_CARDS.map((card) => (
<Card
title={card.TITLE}
description={card.DESCRIPTION}
href={card.HREF}
external={card.HREF.startsWith("http")}
/>
))}
</div>
</section>
</div>
</PageLayout>- Step 2: Commit landing page
git add src/pages/index.astro
git commit -m "feat: hub-style landing page with card grid"Task 5: Resume Page and Experience Detail
Files:
-
Create:
src/pages/resume/index.astro -
Create:
src/pages/resume/[...slug].astro -
Delete:
src/pages/work/index.astro -
Delete:
src/pages/work/[...slug].astro -
Delete:
src/pages/education/index.astro -
Delete:
src/pages/projects/index.astro -
Step 1: Create the resume page
Create src/pages/resume/index.astro:
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import ExperienceEntry from "@components/ExperienceEntry.astro";
import Tag from "@components/Tag.astro";
import { SITE, RESUME } from "@consts";
const allWork = (await getCollection("work"))
.sort((a, b) => new Date(b.data.dateStart).valueOf() - new Date(a.data.dateStart).valueOf());
const allEducation = (await getCollection("education"))
.sort((a, b) => new Date(b.data.dateStart).valueOf() - new Date(a.data.dateStart).valueOf());
const allProjects = (await getCollection("projects"))
.filter((p) => !p.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
const work = await Promise.all(
allWork.map(async (item) => {
const { Content } = await item.render();
return { ...item, Content };
})
);
const education = await Promise.all(
allEducation.map(async (item) => {
const { Content } = await item.render();
return { ...item, Content };
})
);
const skillsCollection = await getCollection("skills");
const skills = skillsCollection.find((entry) => entry.slug === "skills");
const skillList = skills?.body
.split("\n")
.filter((s) => s.trim().startsWith("-"))
.map((s) => s.replace("-", "").trim())
.filter(Boolean) ?? [];
---
<PageLayout title={RESUME.TITLE} description={RESUME.DESCRIPTION}>
<div class="max-w-3xl mx-auto px-6">
<header class="mb-8">
<h1 class="text-3xl font-bold">{SITE.NAME}</h1>
<p class="text-lg text-neutral-500 mt-1">{SITE.DESCRIPTION}</p>
</header>
{skillList.length > 0 && (
<section class="mb-8">
<div class="flex flex-wrap gap-1.5">
{skillList.map((skill) => <Tag tag={skill} linked={false} />)}
</div>
</section>
)}
<section class="mb-10">
<h2 class="text-lg font-semibold border-b border-neutral-200 dark:border-neutral-800 pb-2 mb-2">
Work Experience
</h2>
<div class="divide-y divide-neutral-100 dark:divide-neutral-900">
{work.map((entry) => (
<ExperienceEntry
company={entry.data.company}
role={entry.data.role}
dateStart={entry.data.dateStart}
dateEnd={entry.data.dateEnd}
location={entry.data.location}
tags={entry.data.tags}
slug={entry.slug}
>
<article class="prose dark:prose-invert prose-sm max-w-none">
<entry.Content />
</article>
</ExperienceEntry>
))}
</div>
</section>
<section class="mb-10">
<h2 class="text-lg font-semibold border-b border-neutral-200 dark:border-neutral-800 pb-2 mb-2">
Education
</h2>
<div class="divide-y divide-neutral-100 dark:divide-neutral-900">
{education.map((entry) => (
<ExperienceEntry
company={entry.data.company}
role={entry.data.role}
dateStart={entry.data.dateStart}
dateEnd={entry.data.dateEnd}
tags={entry.data.tags}
collection="education"
>
<article class="prose dark:prose-invert prose-sm max-w-none">
<entry.Content />
</article>
</ExperienceEntry>
))}
</div>
</section>
<section class="mb-10">
<h2 class="text-lg font-semibold border-b border-neutral-200 dark:border-neutral-800 pb-2 mb-2">
Projects
</h2>
<div class="divide-y divide-neutral-100 dark:divide-neutral-900">
{allProjects.map((project) => (
<div class="py-4">
<div class="text-sm text-neutral-500">
{new Intl.DateTimeFormat("en-US", { month: "short", year: "numeric" }).format(project.data.date)}
</div>
<div class="font-semibold text-black dark:text-white">{project.data.title}</div>
<div class="text-sm text-neutral-600 dark:text-neutral-400">{project.data.description}</div>
<div class="flex gap-3 mt-1">
{project.data.demoURL && (
<a href={project.data.demoURL} target="_blank" rel="noopener noreferrer" class="text-sm text-neutral-500 hover:text-black dark:hover:text-white transition-colors">
Demo →
</a>
)}
{project.data.repoURL && (
<a href={project.data.repoURL} target="_blank" rel="noopener noreferrer" class="text-sm text-neutral-500 hover:text-black dark:hover:text-white transition-colors">
Source →
</a>
)}
</div>
</div>
))}
</div>
</section>
</div>
</PageLayout>- Step 2: Create the experience detail page
Create src/pages/resume/[...slug].astro:
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import Tag from "@components/Tag.astro";
import Prose from "@components/Prose.astro";
import { dateRange, readingTime } from "@lib/utils";
export async function getStaticPaths() {
const work = await getCollection("work");
return work.map((entry) => ({
params: { slug: entry.slug },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<PageLayout title={entry.data.company} description={entry.data.role}>
<div class="max-w-3xl mx-auto px-6">
<a
href="/resume"
class="inline-block mb-6 text-sm text-neutral-500 hover:text-black dark:hover:text-white transition-colors"
>
← Back to Resume
</a>
<header class="mb-8">
<h1 class="text-2xl font-bold">{entry.data.company}</h1>
<p class="text-neutral-600 dark:text-neutral-400">{entry.data.role}</p>
<p class="text-sm text-neutral-500 mt-1">
{dateRange(entry.data.dateStart, entry.data.dateEnd)}
{entry.data.location && <span> · {entry.data.location}</span>}
</p>
{entry.data.tags && entry.data.tags.length > 0 && (
<div class="flex flex-wrap gap-1 mt-3">
{entry.data.tags.map((tag) => <Tag tag={tag} />)}
</div>
)}
</header>
<Prose>
<Content />
</Prose>
</div>
</PageLayout>- Step 3: Delete old pages
rm src/pages/work/index.astro src/pages/work/\[...slug\].astro
rm src/pages/education/index.astro
rm src/pages/projects/index.astro
rmdir src/pages/work src/pages/education src/pages/projects- Step 4: Commit resume pages
git add src/pages/resume/ -A
git add src/pages/work src/pages/education src/pages/projects
git commit -m "feat: resume page with work, education, projects and detail view"Task 6: Content Collection Schema Updates
Files:
-
Modify:
content/green/Resume/content/config.ts -
Step 1: Add tags and draft fields; add new collections
Replace content/green/Resume/content/config.ts:
import { defineCollection, z } from "astro:content";
const work = defineCollection({
type: "content",
schema: z.object({
company: z.string(),
role: z.string(),
dateStart: z.coerce.date(),
dateEnd: z.union([z.coerce.date(), z.string()]),
location: z.string(),
tags: z.array(z.string()).optional(),
draft: z.boolean().optional(),
}),
});
const about = defineCollection({
type: "content",
schema: z.object({}),
});
const skills = defineCollection({
type: "content",
schema: z.object({}),
});
const education = defineCollection({
type: "content",
schema: z.object({
company: z.string(),
role: z.string(),
dateStart: z.coerce.date(),
dateEnd: z.union([z.coerce.date(), z.string()]),
tags: z.array(z.string()).optional(),
}),
});
const projects = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string(),
date: z.coerce.date(),
draft: z.boolean().optional(),
demoURL: z.string().optional(),
repoURL: z.string().optional(),
tags: z.array(z.string()).optional(),
}),
});
const scribbles = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string().optional(),
date: z.coerce.date(),
tags: z.array(z.string()).optional(),
draft: z.boolean().optional(),
}),
});
const subroutines = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string().optional(),
date: z.coerce.date(),
tags: z.array(z.string()).optional(),
draft: z.boolean().optional(),
}),
});
const gsoc = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string().optional(),
date: z.coerce.date(),
tags: z.array(z.string()).optional(),
draft: z.boolean().optional(),
}),
});
const now = defineCollection({
type: "content",
schema: z.object({
updated: z.coerce.date().optional(),
}),
});
export const collections = { about, skills, work, projects, education, scribbles, subroutines, gsoc, now };- Step 2: Create placeholder content directories and sample files
Create minimal placeholder content so the site builds:
mkdir -p content/green/Resume/content/scribbles
mkdir -p content/green/Resume/content/subroutines
mkdir -p content/green/Resume/content/gsoc
mkdir -p content/green/Resume/content/nowCreate content/green/Resume/content/scribbles/hello-world.md:
---
title: "Hello World"
description: "A first post placeholder."
date: 2024-01-01
tags: ["travel"]
draft: false
---
This is a placeholder post for the scribbles section.Create content/green/Resume/content/subroutines/getting-started.md:
---
title: "Getting Started with Zephyr"
description: "A placeholder technical writeup."
date: 2024-01-01
tags: ["zephyr", "embedded"]
draft: false
---
This is a placeholder post for the subroutines section.Create content/green/Resume/content/gsoc/intro.md:
---
title: "GSoC 2020 Introduction"
description: "Overview of my Google Summer of Code project."
date: 2020-05-01
tags: ["ros", "robotics"]
draft: false
---
This is a placeholder for the GSoC 2020 section.Create content/green/Resume/content/now/now.md:
---
updated: 2024-01-01
---
[What Shreyas is currently working on — placeholder content.]- Step 3: Commit schema and placeholder content
git add content/green/Resume/content/config.ts
git add content/green/Resume/content/scribbles/ content/green/Resume/content/subroutines/ content/green/Resume/content/gsoc/ content/green/Resume/content/now/
git commit -m "feat: add content schemas for scribbles, subroutines, gsoc, now + placeholder content"Task 7: Blog Section Pages (Scribbles, Subroutines, GSoC)
Files:
-
Create:
src/pages/scribbles/index.astro -
Create:
src/pages/scribbles/[...slug].astro -
Create:
src/pages/subroutines/index.astro -
Create:
src/pages/subroutines/[...slug].astro -
Create:
src/pages/gsoc/index.astro -
Create:
src/pages/gsoc/[...slug].astro -
Step 1: Create scribbles index
Create src/pages/scribbles/index.astro:
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import PostList from "@components/PostList.astro";
import { SCRIBBLES } from "@consts";
const posts = (await getCollection("scribbles"))
.filter((p) => !p.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
---
<PageLayout title={SCRIBBLES.TITLE} description={SCRIBBLES.DESCRIPTION} section="scribbles">
<div class="max-w-3xl mx-auto px-6">
<header class="mb-8">
<h1 class="text-2xl font-bold">Scribbles</h1>
<p class="text-neutral-500 mt-1">Analects of travels and thoughts</p>
</header>
{posts.length > 0 ? (
<PostList posts={posts} basePath="/scribbles" />
) : (
<p class="text-neutral-500">No posts yet.</p>
)}
</div>
</PageLayout>- Step 2: Create scribbles post page
Create src/pages/scribbles/[...slug].astro:
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import Tag from "@components/Tag.astro";
import Prose from "@components/Prose.astro";
import { formatDate, readingTime } from "@lib/utils";
export async function getStaticPaths() {
const posts = await getCollection("scribbles");
return posts
.filter((p) => !p.data.draft)
.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<PageLayout title={post.data.title} description={post.data.description ?? ""} section="scribbles">
<div class="max-w-3xl mx-auto px-6">
<a
href="/scribbles"
class="inline-block mb-6 text-sm text-neutral-500 hover:text-black dark:hover:text-white transition-colors"
>
← Back to Scribbles
</a>
<header class="mb-8">
<h1 class="text-2xl font-bold">{post.data.title}</h1>
<p class="text-sm text-neutral-500 mt-1">
{formatDate(post.data.date)}
<span>·</span>
{readingTime(post.body)}
</p>
{post.data.tags && post.data.tags.length > 0 && (
<div class="flex flex-wrap gap-1 mt-3">
{post.data.tags.map((tag) => <Tag tag={tag} />)}
</div>
)}
</header>
<Prose class="prose-p:font-serif">
<Content />
</Prose>
</div>
</PageLayout>- Step 3: Create subroutines index
Create src/pages/subroutines/index.astro:
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import PostList from "@components/PostList.astro";
import { SUBROUTINES } from "@consts";
const posts = (await getCollection("subroutines"))
.filter((p) => !p.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
---
<PageLayout title={SUBROUTINES.TITLE} description={SUBROUTINES.DESCRIPTION} section="subroutines">
<div class="max-w-3xl mx-auto px-6">
<header class="mb-8">
<h1 class="text-2xl font-bold">Subroutines</h1>
<p class="text-neutral-500 mt-1">Technical writeups and deep dives</p>
</header>
{posts.length > 0 ? (
<PostList posts={posts} basePath="/subroutines" />
) : (
<p class="text-neutral-500">No posts yet.</p>
)}
</div>
</PageLayout>- Step 4: Create subroutines post page
Create src/pages/subroutines/[...slug].astro:
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import Tag from "@components/Tag.astro";
import Prose from "@components/Prose.astro";
import { formatDate, readingTime } from "@lib/utils";
export async function getStaticPaths() {
const posts = await getCollection("subroutines");
return posts
.filter((p) => !p.data.draft)
.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<PageLayout title={post.data.title} description={post.data.description ?? ""} section="subroutines">
<div class="max-w-3xl mx-auto px-6">
<a
href="/subroutines"
class="inline-block mb-6 text-sm text-neutral-500 hover:text-black dark:hover:text-white transition-colors"
>
← Back to Subroutines
</a>
<header class="mb-8">
<h1 class="text-2xl font-bold">{post.data.title}</h1>
<p class="text-sm text-neutral-500 mt-1">
{formatDate(post.data.date)}
<span>·</span>
{readingTime(post.body)}
</p>
{post.data.tags && post.data.tags.length > 0 && (
<div class="flex flex-wrap gap-1 mt-3">
{post.data.tags.map((tag) => <Tag tag={tag} />)}
</div>
)}
</header>
<Prose>
<Content />
</Prose>
</div>
</PageLayout>- Step 5: Create GSoC index
Create src/pages/gsoc/index.astro:
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import PostList from "@components/PostList.astro";
import { GSOC } from "@consts";
const posts = (await getCollection("gsoc"))
.filter((p) => !p.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
---
<PageLayout title={GSOC.TITLE} description={GSOC.DESCRIPTION}>
<div class="max-w-3xl mx-auto px-6">
<header class="mb-8">
<h1 class="text-2xl font-bold">GSoC 2020</h1>
<p class="text-neutral-500 mt-1">Google Summer of Code 2020</p>
</header>
{posts.length > 0 ? (
<PostList posts={posts} basePath="/gsoc" />
) : (
<p class="text-neutral-500">No posts yet.</p>
)}
</div>
</PageLayout>- Step 6: Create GSoC post page
Create src/pages/gsoc/[...slug].astro:
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import Tag from "@components/Tag.astro";
import Prose from "@components/Prose.astro";
import { formatDate, readingTime } from "@lib/utils";
export async function getStaticPaths() {
const posts = await getCollection("gsoc");
return posts
.filter((p) => !p.data.draft)
.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<PageLayout title={post.data.title} description={post.data.description ?? ""}>
<div class="max-w-3xl mx-auto px-6">
<a
href="/gsoc"
class="inline-block mb-6 text-sm text-neutral-500 hover:text-black dark:hover:text-white transition-colors"
>
← Back to GSoC 2020
</a>
<header class="mb-8">
<h1 class="text-2xl font-bold">{post.data.title}</h1>
<p class="text-sm text-neutral-500 mt-1">
{formatDate(post.data.date)}
<span>·</span>
{readingTime(post.body)}
</p>
{post.data.tags && post.data.tags.length > 0 && (
<div class="flex flex-wrap gap-1 mt-3">
{post.data.tags.map((tag) => <Tag tag={tag} />)}
</div>
)}
</header>
<Prose>
<Content />
</Prose>
</div>
</PageLayout>- Step 7: Commit blog sections
git add src/pages/scribbles/ src/pages/subroutines/ src/pages/gsoc/
git commit -m "feat: add scribbles, subroutines, and gsoc blog sections"Task 8: Static Pages (Now, Contact, FAQ, Domain)
Files:
-
Create:
src/pages/now.astro -
Create:
src/pages/contact.astro -
Create:
src/pages/faq/index.astro -
Create:
src/pages/domain.astro -
Step 1: Create Now page
Create src/pages/now.astro:
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import Prose from "@components/Prose.astro";
import { formatDate } from "@lib/utils";
import { NOW } from "@consts";
const nowCollection = await getCollection("now");
const nowEntry = nowCollection[0];
const { Content } = nowEntry ? await nowEntry.render() : { Content: null };
---
<PageLayout title={NOW.TITLE} description={NOW.DESCRIPTION}>
<div class="max-w-3xl mx-auto px-6">
<header class="mb-8">
<h1 class="text-2xl font-bold">Now</h1>
{nowEntry?.data.updated && (
<p class="text-sm text-neutral-500 mt-1">
Updated {formatDate(nowEntry.data.updated)}
</p>
)}
</header>
{Content ? (
<Prose>
<Content />
</Prose>
) : (
<p class="text-neutral-500">Nothing here yet.</p>
)}
</div>
</PageLayout>- Step 2: Create Contact page
Create src/pages/contact.astro:
---
import PageLayout from "@layouts/PageLayout.astro";
import { CONTACT, SOCIALS } from "@consts";
---
<PageLayout title={CONTACT.TITLE} description={CONTACT.DESCRIPTION}>
<div class="max-w-3xl mx-auto px-6">
<header class="mb-8">
<h1 class="text-2xl font-bold">Contact</h1>
<p class="text-neutral-500 mt-1">[Your contact intro here]</p>
</header>
<form class="space-y-4 max-w-lg">
<div>
<label for="name" class="block text-sm font-medium mb-1">Name</label>
<input
type="text"
id="name"
name="name"
required
class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-900 text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-neutral-400"
/>
</div>
<div>
<label for="email" class="block text-sm font-medium mb-1">Email</label>
<input
type="email"
id="email"
name="email"
required
class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-900 text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-neutral-400"
/>
</div>
<div>
<label for="message" class="block text-sm font-medium mb-1">Message</label>
<textarea
id="message"
name="message"
rows="5"
required
class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-900 text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-neutral-400 resize-y"
></textarea>
</div>
<button
type="submit"
class="px-6 py-2 bg-black dark:bg-white text-white dark:text-black rounded-lg font-medium hover:opacity-80 transition-opacity"
>
Send
</button>
</form>
<div class="mt-8 pt-6 border-t border-neutral-200 dark:border-neutral-800">
<p class="text-sm text-neutral-500 mb-3">Or find me on</p>
<div class="flex gap-4">
{SOCIALS.map((social) => (
<a
href={social.HREF}
target="_blank"
rel="noopener noreferrer"
class="text-sm text-neutral-500 hover:text-black dark:hover:text-white transition-colors"
>
{social.NAME}
</a>
))}
</div>
</div>
</div>
</PageLayout>- Step 3: Create FAQ page
Create src/pages/faq/index.astro:
---
import PageLayout from "@layouts/PageLayout.astro";
import { FAQ } from "@consts";
---
<PageLayout title={FAQ.TITLE} description={FAQ.DESCRIPTION}>
<div class="max-w-3xl mx-auto px-6">
<header class="mb-8">
<h1 class="text-2xl font-bold">Masters in Europe FAQ</h1>
<p class="text-neutral-500 mt-1">Frequently asked questions about studying in Europe via EIT Digital and similar programs.</p>
</header>
<p class="text-neutral-500">[FAQ content will go here — placeholder for now.]</p>
</div>
</PageLayout>- Step 4: Create Domain Map placeholder page
Create src/pages/domain.astro:
---
import PageLayout from "@layouts/PageLayout.astro";
---
<PageLayout title="Domain Map" description="Areas of expertise and interest.">
<div class="max-w-3xl mx-auto px-6">
<header class="mb-8">
<h1 class="text-2xl font-bold">Domain of Shreyas</h1>
<p class="text-neutral-500 mt-1">A map of expertise areas and interests.</p>
</header>
<div class="border border-neutral-200 dark:border-neutral-800 rounded-lg p-12 flex items-center justify-center">
<p class="text-neutral-400 text-center">
[Interactive domain map — coming soon]<br/>
<span class="text-sm">A curated visualization of expertise domains, linking to relevant experience and projects.</span>
</p>
</div>
</div>
</PageLayout>- Step 5: Commit static pages
git add src/pages/now.astro src/pages/contact.astro src/pages/faq/ src/pages/domain.astro
git commit -m "feat: add now, contact, FAQ, and domain map placeholder pages"Task 9: Tags Pages
Files:
-
Create:
src/pages/tags/index.astro -
Create:
src/pages/tags/[tag].astro -
Step 1: Create tags index page
Create src/pages/tags/index.astro:
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import Tag from "@components/Tag.astro";
import { TAGS } from "@consts";
const collections = ["work", "education", "projects", "scribbles", "subroutines", "gsoc"] as const;
const tagCounts = new Map<string, number>();
for (const name of collections) {
const entries = await getCollection(name);
for (const entry of entries) {
const tags = entry.data.tags ?? [];
for (const tag of tags) {
const lower = tag.toLowerCase();
tagCounts.set(lower, (tagCounts.get(lower) ?? 0) + 1);
}
}
}
const sortedTags = [...tagCounts.entries()].sort((a, b) => b[1] - a[1]);
---
<PageLayout title={TAGS.TITLE} description={TAGS.DESCRIPTION}>
<div class="max-w-3xl mx-auto px-6">
<header class="mb-8">
<h1 class="text-2xl font-bold">Tags</h1>
</header>
<div class="flex flex-wrap gap-2">
{sortedTags.map(([tag, count]) => (
<a
href={`/tags/${tag}`}
class="inline-flex items-center gap-1"
>
<Tag tag={tag} linked={false} />
<span class="text-xs text-neutral-400">{count}</span>
</a>
))}
</div>
</div>
</PageLayout>- Step 2: Create tag detail page
Create src/pages/tags/[tag].astro:
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import Tag from "@components/Tag.astro";
import { formatDate } from "@lib/utils";
const collectionNames = ["work", "education", "projects", "scribbles", "subroutines", "gsoc"] as const;
type TaggedItem = {
title: string;
type: string;
date: Date;
href: string;
};
export async function getStaticPaths() {
const collectionNames = ["work", "education", "projects", "scribbles", "subroutines", "gsoc"] as const;
const allTags = new Set<string>();
for (const name of collectionNames) {
const entries = await getCollection(name);
for (const entry of entries) {
const tags = entry.data.tags ?? [];
tags.forEach((t: string) => allTags.add(t.toLowerCase()));
}
}
return [...allTags].map((tag) => ({
params: { tag },
}));
}
const { tag } = Astro.params;
const items: TaggedItem[] = [];
for (const name of collectionNames) {
const entries = await getCollection(name);
for (const entry of entries) {
const tags = (entry.data.tags ?? []).map((t: string) => t.toLowerCase());
if (tags.includes(tag)) {
const data = entry.data as Record<string, unknown>;
const title = (data.title ?? data.company ?? entry.slug) as string;
const date = (data.date ?? data.dateStart) as Date;
const typeLabels: Record<string, string> = {
work: "Work",
education: "Education",
projects: "Project",
scribbles: "Scribble",
subroutines: "Subroutine",
gsoc: "GSoC",
};
const hrefMap: Record<string, string> = {
work: `/resume/${entry.slug}`,
education: `/resume`,
projects: `/resume`,
scribbles: `/scribbles/${entry.slug}`,
subroutines: `/subroutines/${entry.slug}`,
gsoc: `/gsoc/${entry.slug}`,
};
items.push({
title,
type: typeLabels[name] ?? name,
date,
href: hrefMap[name] ?? "/",
});
}
}
}
items.sort((a, b) => b.date.valueOf() - a.date.valueOf());
---
<PageLayout title={`Tagged: ${tag}`} description={`All content tagged with "${tag}".`}>
<div class="max-w-3xl mx-auto px-6">
<header class="mb-8 flex items-center gap-3">
<h1 class="text-2xl font-bold">Tagged:</h1>
<Tag tag={tag} linked={false} />
</header>
<a
href="/tags"
class="inline-block mb-6 text-sm text-neutral-500 hover:text-black dark:hover:text-white transition-colors"
>
← All tags
</a>
<ul class="divide-y divide-neutral-200 dark:divide-neutral-800">
{items.map((item) => (
<li class="py-3">
<div class="flex items-center gap-2 text-sm text-neutral-500">
<span class="font-medium">{item.type}</span>
<span>·</span>
<span>{formatDate(item.date)}</span>
</div>
<a
href={item.href}
class="font-medium text-black dark:text-white hover:opacity-70 transition-opacity"
>
{item.title}
</a>
</li>
))}
</ul>
</div>
</PageLayout>- Step 3: Commit tags pages
git add src/pages/tags/
git commit -m "feat: add tags index and tag detail pages"Task 10: Cleanup and Config Updates
Files:
-
Modify:
astro.config.mjs -
Modify:
src/components/Head.astro -
Delete:
src/components/HomeContainer.astro -
Delete:
src/components/BackToTop.astro -
Delete:
src/components/BackToPrev.astro -
Delete:
src/lib/skillIcons.ts -
Delete:
src/components/Container.astro(if no longer used) -
Delete:
src/components/Section.astro(if no longer used) -
Delete:
src/components/Link.astro(if no longer used) -
Step 1: Update astro.config.mjs site URL
In astro.config.mjs, change the site URL:
site: "https://shreyasgokhale.com",- Step 2: Delete unused components and files
rm src/components/HomeContainer.astro
rm src/components/BackToTop.astro
rm src/components/BackToPrev.astro
rm src/lib/skillIcons.ts
rm src/components/Container.astro
rm src/components/Section.astro
rm src/components/Link.astro- Step 3: Update Header component
The old Header.astro is now replaced by Nav.astro. Either delete Header.astro or replace it with a redirect import. Since PageLayout.astro now imports Nav.astro instead, just delete the old one:
rm src/components/Header.astro- Step 4: Remove old ArrowCard if no longer needed
Check if ArrowCard.astro is still used. Since the landing page now uses Card.astro and the resume page lists projects inline, it’s no longer needed:
rm src/components/ArrowCard.astro- Step 5: Remove PageFind component if search is still done via integration
The Pagefind integration handles search automatically. If PageFind.astro was only used in the old header, and the new Nav doesn’t include it yet, keep it for now but remove it from deleted components. Pagefind integration still runs at build time. If you want search in the nav, add it to Nav.astro later.
rm src/components/PageFind.astro- Step 6: Commit cleanup
git add -A
git commit -m "chore: remove unused components, update site URL"Task 11: Build Verification and Fix
Files: None (verification only)
- Step 1: Run the build
npm run buildExpected: Build completes without errors. If there are import errors for deleted components, fix them.
- Step 2: Start dev server and verify all routes
npm run devCheck each route manually in the browser:
-
/— Hub landing with card grid -
/resume— Full resume with work, education, projects -
/resume/[any-work-slug]— Experience detail -
/scribbles— Blog index -
/scribbles/hello-world— Blog post with serif font -
/subroutines— Tech blog index -
/subroutines/getting-started— Tech post -
/gsoc— GSoC index -
/gsoc/intro— GSoC post -
/now— Now page -
/contact— Contact form -
/faq— FAQ placeholder -
/domain— Domain map placeholder -
/tags— All tags with counts -
/tags/[tag]— Filtered content by tag -
Step 3: Test dark mode toggle
Click the theme toggle in the nav. Verify all pages switch between light and dark mode correctly.
- Step 4: Test mobile responsiveness
Resize browser to mobile width. Verify:
-
Hamburger menu appears and works
-
Card grid becomes single column
-
All content flows correctly on narrow screens
-
Step 5: Test print (resume only)
Go to /resume, press Ctrl+P (or Cmd+P on Mac). Verify:
-
Nav and footer are hidden
-
Colors are black and white
-
Layout is tight and professional
-
“Read more” links are hidden
-
Step 6: Fix any issues found, commit
git add -A
git commit -m "fix: resolve build and route issues from redesign"Summary of Routes After Implementation
| Route | Status |
|---|---|
/ | Hub landing page |
/resume | Printable resume |
/resume/[slug] | Experience detail |
/scribbles | Personal blog index |
/scribbles/[slug] | Blog post (serif) |
/subroutines | Technical blog index |
/subroutines/[slug] | Technical post |
/gsoc | GSoC 2020 index |
/gsoc/[slug] | GSoC post |
/faq | Masters FAQ |
/now | Now page |
/contact | Contact form |
/domain | Placeholder |
/tags | Tag index |
/tags/[tag] | Tag detail |