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

FileResponsibility
src/pages/index.astroHub landing page with intro + card grid
src/pages/resume/index.astroFull resume page (printable)
src/pages/resume/[...slug].astroExperience detail page
src/pages/scribbles/index.astroBlog index (personal writing)
src/pages/scribbles/[...slug].astroIndividual blog post
src/pages/subroutines/index.astroTechnical writeups index
src/pages/subroutines/[...slug].astroIndividual technical post
src/pages/gsoc/index.astroGSoC 2020 index
src/pages/gsoc/[...slug].astroIndividual GSoC post
src/pages/faq/index.astroMasters FAQ page
src/pages/now.astroNow page
src/pages/contact.astroContact form page
src/pages/tags/index.astroAll tags with counts
src/pages/tags/[tag].astroContent filtered by tag
src/pages/domain.astroDomain map placeholder
src/components/Nav.astroNew top navigation bar
src/components/Card.astroLanding page hub card
src/components/Tag.astroPill-shaped tag with accent color
src/components/ExperienceEntry.astroWork/education entry for resume
src/components/PostList.astroPost listing for blog indexes
src/components/Prose.astroMarkdown content wrapper
src/components/ThemeToggle.astroSun/moon theme toggle
src/components/MobileMenu.astroHamburger menu for mobile nav
src/lib/tags.tsTag color mapping and utilities

Files to Modify

FileChanges
src/styles/global.cssNew base styles, print stylesheet, section CSS vars
src/consts.tsNew route metadata, social links, hub card definitions
src/types.tsNew types for cards, tags, nav items
src/layouts/PageLayout.astroUse new Nav/Footer, accept section prop
src/components/Footer.astroRedesign: social icons + copyright, move theme toggle to nav
src/components/Head.astroRemove Devicon CDN, update site metadata
tailwind.config.mjsAdd domain accent colors
astro.config.mjsUpdate site URL
content/green/Resume/content/config.tsAdd tags and draft to work/education schemas; add scribbles, subroutines, gsoc, now collections

Files to Delete

FileReason
src/pages/work/index.astroReplaced by /resume
src/pages/work/[...slug].astroReplaced by /resume/[...slug]
src/pages/education/index.astroMerged into /resume
src/pages/projects/index.astroMerged into /resume
src/components/HomeContainer.astroReplaced by new landing layout
src/components/BackToTop.astroRemoved per minimalist design
src/components/BackToPrev.astroReplaced by inline back links
src/lib/skillIcons.tsDevicon 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">
      &copy; {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> &middot; {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 &rarr;
    </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 &rarr;
                </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 &rarr;
                </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"
    >
      &larr; 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> &middot; {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/now

Create 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"
    >
      &larr; 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>&middot;</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"
    >
      &larr; 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>&middot;</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"
    >
      &larr; 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>&middot;</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"
    >
      &larr; 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>&middot;</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 build

Expected: 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 dev

Check 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

RouteStatus
/Hub landing page
/resumePrintable resume
/resume/[slug]Experience detail
/scribblesPersonal blog index
/scribbles/[slug]Blog post (serif)
/subroutinesTechnical blog index
/subroutines/[slug]Technical post
/gsocGSoC 2020 index
/gsoc/[slug]GSoC post
/faqMasters FAQ
/nowNow page
/contactContact form
/domainPlaceholder
/tagsTag index
/tags/[tag]Tag detail