All powerups

@liolocs/astro-personal-website

Personal website boilerplate made in astro with brutalism UI style

v1.0.3MITsingle-use
bash
pup install @liolocs/astro-personal-website

Variables

  • projectName(required)
  • name(required)
  • siteDescription(required)
  • twitterHandle(optional)
  • linkedinHandle(optional)
  • youtubeHandle(optional)
  • githubHandle(optional)
  • email(optional)
  • profileImage(optional)
  • umamiWebsiteId(optional)

Steps

1create .agents/skills/astro/SKILL.md
.agents/skills/astro/SKILL.md
---
name: astro
description: Skill for building with the Astro web framework. Helps create Astro components and pages, configure SSR adapters, set up content collections, deploy static sites, and manage project structure and CLI commands. Use when the user needs to work with Astro, mentions .astro files, asks about static site generation (SSG), islands architecture, content collections, or deploying an Astro project.
license: MIT
metadata: 
  authors: "Astro Team"
  version: "0.0.1"
---

# Astro Usage Guide

**Always consult [docs.astro.build](https://docs.astro.build) for code examples and latest API.**

Astro is the web framework for content-driven websites.

---

## Quick Reference

### File Location
CLI looks for `astro.config.js`, `astro.config.mjs`, `astro.config.cjs`, and `astro.config.ts` in: `./`. Use `--config` for custom path.

### CLI Commands

- `npx astro dev` - Start the development server.
- `npx astro build` - Build your project and write it to disk.
- `npx astro check` - Check your project for errors.
- `npx astro add` - Add an integration.
- `npx astro sync` - Generate TypeScript types for all Astro modules.

**Re-run after adding/changing plugins.**

### Project Structure

Reference [project structure docs](https://docs.astro.build/en/basics/project-structure).

- `src/*` - Project source code (components, pages, styles, images, etc.)
- `src/pages` - **Required.** Defines all pages and routes.
- `src/components` - Components (convention, not required).
- `src/layouts` - Layout components (convention, not required).
- `src/styles` - CSS/Sass files (convention, not required).
- `public/*` - Non-code, unprocessed assets (fonts, icons, etc.); copied as-is to build output.
- `package.json` - Project manifest.
- `astro.config.{js,mjs,cjs,ts}` - Astro configuration file. (recommended)
- `tsconfig.json` - TypeScript configuration file. (recommended)

---

## Core Config Options

| Option | Notes |
|--------|-------|
| `site` | Your final, deployed URL. Used to generate sitemaps and canonical URLs. |

### Example `astro.config.ts`

```ts
import { defineConfig } from 'astro/config';

export default defineConfig({
  site: 'https://example.com',
});
```

---

## Common Workflows

### Creating a Basic Page

Add a file to `src/pages/` — the filename becomes the route:

```astro
---
// src/pages/index.astro
const title = 'Hello, Astro!';
---
<html>
  <head><title>{title}</title></head>
  <body>
    <h1>{title}</h1>
  </body>
</html>
```

### Creating a Component

```astro
---
// src/components/Card.astro
const { title, body } = Astro.props;
---
<div class="card">
  <h2>{title}</h2>
  <p>{body}</p>
</div>
```

### Deploying with an Adapter

1. Add the adapter: `npx astro add vercel --yes` (or `node`, `cloudflare`, `netlify`)
2. Run `npx astro check` to catch type and configuration errors before building.
3. Run `npx astro build` to produce the deployment artifact.
4. Verify the build output directory (e.g. `dist/`) exists and is non-empty before proceeding.
5. Deploy the output per the adapter's documentation.

---

## Adapters

Deploy to your favorite server, serverless, or edge host with build adapters. Use an adapter to enable on-demand rendering in your Astro project.

**Add [Node.js](https://docs.astro.build/en/guides/integrations-guide/node) adapter using astro add:**
```
npx astro add node --yes
```

**Add [Cloudflare](https://docs.astro.build/en/guides/integrations-guide/cloudflare) adapter using astro add:**
```
npx astro add cloudflare --yes
```

**Add [Netlify](https://docs.astro.build/en/guides/integrations-guide/netlify) adapter using astro add:**
```
npx astro add netlify --yes
```

**Add [Vercel](https://docs.astro.build/en/guides/integrations-guide/vercel) adapter using astro add:**
```
npx astro add vercel --yes
```

[Other Community adapters](https://astro.build/integrations/2/?search=&categories%5B%5D=adapters)

## Resources

- [Docs](https://docs.astro.build)
- [Config Reference](https://docs.astro.build/en/reference/configuration-reference/)
- [llms.txt](https://docs.astro.build/llms.txt)
- [GitHub](https://github.com/withastro/astro)
2create .gitignore
.gitignore
3create .mcp.json
.mcp.json
{
  "mcpServers": {
    "Astro docs": {
      "type": "http",
      "url": "https://mcp.docs.astro.build/mcp"
    }
  }
}
4create .prettierignore
.prettierignore
node_modules/
coverage/
.pnpm-store/
pnpm-lock.yaml
package-lock.json
yarn.lock
5create .prettierrc
.prettierrc
{
  "endOfLine": "lf",
  "semi": false,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 80,
  "plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
  "tailwindStylesheet": "src/styles/global.css",
  "tailwindFunctions": ["cn", "cva"],
  "overrides": [
    {
      "files": "*.astro",
      "options": {
        "parser": "astro"
      }
    }
  ]
}
6create README.md
README.md
# Personal Website

A personal website boilerplate — brutalism style. Built with
[Astro](https://astro.build) + React + Tailwind CSS v4 + shadcn/ui, deployed to
[Cloudflare Workers](https://developers.cloudflare.com/workers/).

> Scaffolded by the `astro-personal-website` powerup.

## Getting started

```sh
npm install
npm run setup   # authenticate with Cloudflare (runs `wrangler login`)
npm run dev     # dev server at http://localhost:4321
```

## Production site URL

The sitemap integration requires a `site` URL at build time. Set `SITE_URL`
once your Cloudflare domain (or `*.workers.dev` URL) is known, then build:

```sh
SITE_URL=https://your-domain.example npm run build
```

Dev builds fall back to `http://localhost:4321`, so no setup is needed locally.

## Deploying

Deploying uses [wrangler](https://developers.cloudflare.com/workers/), which is
authenticated by `npm run setup`. Create your own `wrangler.jsonc` (or
`wrangler.toml`) in the project root for your Worker, then:

```sh
npm run deploy   # astro build && wrangler deploy
```

## Make it yours

Personal bits are applied at powerup-apply time via variables. To change them
afterwards:

| What                          | Where                              |
| ----------------------------- | ---------------------------------- |
| Site title & description      | `src/consts.ts`                    |
| Name, profile image, socials  | `src/pages/index.astro`            |
| Home content page             | `src/content/pages/home.mdx`       |
| Analytics (Umami)             | `src/components/BaseHead.astro`    |
| Favicons                      | `public/favicon.svg`, `public/favicon.ico` |

Social buttons only render for handles you provided; add more like:

```astro
<SocialButton type="github" handle="your-handle">Github @your-handle</SocialButton>
```

To use your own profile picture, drop an image into `public/` and pass it to
the `Heading` component: `image={{ src: "/your-photo.png", alt: "Profile Picture" }}`.
Without it, a generic placeholder is shown.

## Project structure

```text
├── public/               # static assets (favicons, webmanifest)
├── src/
│   ├── components/       # Astro + React + shadcn/ui components
│   ├── content/pages/    # MDX content pages (home)
│   ├── layouts/          # main layout, incl. <BaseHead /> meta tags
│   ├── pages/            # routes (index.astro)
│   ├── styles/           # Tailwind + global styles
│   └── consts.ts         # site title & description
└── wrangler.jsonc        # your own Cloudflare config (create after `npm run setup`)
```

## Commands

| Command                     | Action                                            |
| :-------------------------- | :------------------------------------------------ |
| `npm run setup`             | Authenticate with Cloudflare (`wrangler login`)   |
| `npm run dev`               | Start local dev server at `localhost:4321`        |
| `npm run build`             | Build your production site to `./dist/`           |
| `npm run preview`           | Preview your build locally, before deploying      |
| `npm run deploy`            | Build and deploy to Cloudflare Workers            |
| `npm run astro ...`         | Run CLI commands like `astro add`, `astro check`  |
| `npm run lint` / `format`   | ESLint / Prettier                                 |
| `npm run typecheck`         | `astro check`                                     |
| `npm run cf-typegen`        | Generate Cloudflare types (`wrangler types`)      |

## Learn more

- [Astro docs](https://docs.astro.build)
- [Cloudflare Workers docs](https://developers.cloudflare.com/workers/)
- [Tailwind CSS docs](https://tailwindcss.com/docs)
7create astro.config.mjs
astro.config.mjs
// @ts-check

import tailwindcss from "@tailwindcss/vite"
import { defineConfig } from "astro/config"
import react from "@astrojs/react"
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';

import cloudflare from "@astrojs/cloudflare";

// Set SITE_URL in your production build environment once your Cloudflare
// domain is set up (run `npm run setup` first to authenticate with wrangler).
const site = process.env.SITE_URL || "http://localhost:4321"

// https://astro.build/config
export default defineConfig({
  site,

  vite: {
    plugins: [tailwindcss()],
  },

  integrations: [react(), mdx(), sitemap()],
  adapter: cloudflare(),
})
8create components.json
components.json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "base-sera",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "src/styles/global.css",
    "baseColor": "zinc",
    "cssVariables": true,
    "prefix": ""
  },
  "iconLibrary": "phosphor",
  "rtl": false,
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  },
  "menuColor": "default",
  "menuAccent": "subtle",
  "registries": {}
}
9create eslint.config.js
eslint.config.js
import js from "@eslint/js"
import globals from "globals"
import reactHooks from "eslint-plugin-react-hooks"
import reactRefresh from "eslint-plugin-react-refresh"
import tseslint from "typescript-eslint"
import { defineConfig, globalIgnores } from "eslint/config"

export default defineConfig([
  globalIgnores(["dist", ".astro"]),
  {
    files: ["**/*.{ts,tsx}"],
    extends: [
      js.configs.recommended,
      tseslint.configs.recommended,
      reactHooks.configs.flat.recommended,
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      globals: globals.browser,
    },
  },
])
10dynamic-create package.json
package.json
{
  "name": "{{projectName}}",
  "type": "module",
  "version": "0.0.1",
  "private": true,
  "engines": {
    "node": ">=22.12.0"
  },
  "scripts": {
    "setup": "wrangler login",
    "dev": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "astro": "astro",
    "lint": "eslint .",
    "format": "prettier --write \"**/*.{ts,tsx,astro}\"",
    "typecheck": "astro check",
    "generate-types": "wrangler types",
    "deploy": "astro build && wrangler deploy",
    "cf-typegen": "wrangler types"
  },
  "dependencies": {
    "@astrojs/cloudflare": "^14.2.5",
    "@astrojs/mdx": "^7.0.8",
    "@astrojs/react": "^6.0.4",
    "@astrojs/sitemap": "^3.7.3",
    "@base-ui/react": "^1.7.0",
    "@fontsource-variable/dm-sans": "^5.3.0",
    "@fontsource-variable/inter": "^5.3.0",
    "@phosphor-icons/react": "^2.1.10",
    "@radix-ui/react-dropdown-menu": "^2.1.24",
    "@radix-ui/react-slot": "^1.3.3",
    "@shadcn/react": "^0.3.0",
    "@tailwindcss/typography": "^0.5.20",
    "@tailwindcss/vite": "^4",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "astro": "^7.2.6",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "^1.1.1",
    "date-fns": "^4.4.0",
    "embla-carousel-react": "^8.6.0",
    "input-otp": "^1.5.0",
    "react": "^19.2.6",
    "react-day-picker": "^10.0.1",
    "react-dom": "^19.2.6",
    "react-resizable-panels": "^4.12.3",
    "recharts": "3.8.0",
    "shadcn": "^4.19.0",
    "tailwind-merge": "^3.6.0",
    "tailwindcss": "^4",
    "tw-animate-css": "^1.4.0"
  },
  "devDependencies": {
    "@astrojs/check": "^0.9.10",
    "@eslint/js": "^10",
    "eslint": "^10",
    "eslint-plugin-react-hooks": "^7.1.1",
    "eslint-plugin-react-refresh": "^0.5.2",
    "globals": "^17",
    "prettier": "^3.8.3",
    "prettier-plugin-astro": "^0.14.1",
    "prettier-plugin-tailwindcss": "^0.8.0",
    "typescript": "~6",
    "typescript-eslint": "^8",
    "wrangler": "^4.127.0"
  }
}
11create public/favicon.ico
public/favicon.ico
�PNG


IHDR  D����PLTE���EEE###���������444������fff���fff���VVVvvv���www���.gggAAu=�2p�%]jI=]K�%y���!	"
�8��3]�.^�']|!tJ �"Sz hwG�4�?0Z3L3	�3t \F.�0�����:��;��:��;��<��<��=��:��>��@��7q�9��9��7j�:��;��<��<��=��8y4�+�?tRNS��������9�V������r�s������������������������������������������HwIDATx�}��n!�;�o�'vz����{��_"-����B��eFDa��D�&,��V!X���Q<�D(�RO�.�q��3�Hi��������yEe@�2hL��1e���JBUZ����B�p��<�E�t���zNeI�nyꖛ�v��z�EŤ���i(S�Ϳ��Ν-,^>/�����vu}��
�����ϯ�Mlmc���n{���D��Y�ONQ�#���e1���(�����60��IEND�B`�
12create public/favicon.svg
public/favicon.svg
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
    <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
    <style>
        path { fill: #000; }
        @media (prefers-color-scheme: dark) {
            path { fill: #FFF; }
        }
    </style>
</svg>
13create public/site.webmanifest
public/site.webmanifest
{"name":"","short_name":"","icons":[{"src":"/favicon.svg","sizes":"any","type":"image/svg+xml"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
14dynamic-create src/components/BaseHead.astro
src/components/BaseHead.astro
\n{%- endif %}\n"
---
// Import the global.css file here so that it is included on
// all pages through the use of the <BaseHead /> component.
import "../styles/global.css"
import type { ImageMetadata } from "astro"
import { SITE_TITLE } from "../consts"

interface Props {
  title: string
  description: string
  image?: ImageMetadata
}

const canonicalURL = new URL(Astro.url.pathname, Astro.site)

const { title, description, image = "https://placehold.co/150x150?text=+" } = Astro.props
---

<!-- Global Metadata -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="sitemap" href="/sitemap-index.xml" />
<link
  rel="alternate"
  type="application/rss+xml"
  title={SITE_TITLE}
  href={new URL("rss.xml", Astro.site)}
/>
<meta name="generator" content={Astro.generator} />

<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />

<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="description" content={description} />

<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image.src, Astro.url)} />

<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
{%- if umamiWebsiteId %}
<script
  defer
  src="https://cloud.umami.is/script.js"
  data-website-id="{{umamiWebsiteId}}"></script>
{%- endif %}
15create src/components/ModeToggle.tsx
src/components/ModeToggle.tsx
import * as React from "react"
import { MoonIcon, SunIcon } from "@phosphor-icons/react"

import { Button } from "@/components/ui/button"

export function ModeToggle() {
  const [theme, setThemeState] = React.useState<
    "theme-light" | "dark" | "system"
  >("theme-light")

  React.useEffect(() => {
    const isDarkMode = document.documentElement.classList.contains("dark")
    setThemeState(isDarkMode ? "dark" : "theme-light")
  }, [])

  React.useEffect(() => {
    const isDark =
      theme === "dark" ||
      (theme === "system" &&
        window.matchMedia("(prefers-color-scheme: dark)").matches)
    document.documentElement.classList[isDark ? "add" : "remove"]("dark")
  }, [theme])

  return theme === "theme-light" ? (
      <Button onClick={() => setThemeState("dark")} variant="neutral" size="icon" className="bg-foreground text-secondary-background cursor-pointer">
        <MoonIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
        <span className="sr-only">Toggle theme</span>
      </Button>
    ) : (
      <Button onClick={() => setThemeState("theme-light")} variant="neutral" size="icon" className="bg-foreground text-secondary-background cursor-pointer">
        <SunIcon className="h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
        <span className="sr-only">Toggle theme</span>
      </Button>
    )  
}
16create src/components/buttons/social-button.astro
src/components/buttons/social-button.astro
---
import { Button } from "@/components/ui/button.tsx"
import { SocialIcon } from "@/components/icons/social-icons.tsx"
import type { SocialIcons } from "@/components/icons/social-icons.tsx"
import { ArrowSquareOutIcon } from "@phosphor-icons/react"

interface Props {
  type: SocialIcons
  handle: string
}

const { type, handle } = Astro.props

const socialMap: Record<
  SocialIcons,
  { link: string; icon: SocialIcons; color: string }
> = {
  twitter: {
    link: "https://twitter.com/" + handle,
    icon: "twitter",
    color: "bg-white",
  },
  linkedin: {
    link: "https://linkedin.com/in/" + handle,
    icon: "linkedin",
    color: "bg-white",
  },
  youtube: {
    link: "https://youtube.com/@" + handle,
    icon: "youtube",
    color: "bg-white",
  },
  twitch: {
    link: "https://twitch.com/" + handle,
    icon: "twitch",
    color: "bg-white",
  },
  instagram: {
    link: "https://instagram.com/" + handle,
    icon: "instagram",
    color: "bg-white",
  },
  threads: {
    link: "https://threads.com/" + handle,
    icon: "threads",
    color: "bg-white",
  },
  discord: {
    link: "https://discord.com/" + handle,
    icon: "discord",
    color: "bg-white",
  },
  github: {
    link: "https://github.com/" + handle,
    icon: "github",
    color: "bg-white",
  },
  email: {
    link: "mailto:" + handle,
    icon: "email",
    color: "bg-white",
  },
}

const href = socialMap[type].link
const icon = socialMap[type].icon
const color = socialMap[type].color
---

<Button className={"flex w-full gap-4 p-0 " + color} size="lg">
  <a
    href={href}
    target="_blank"
    rel="noreferrer"
    class:list={"grid w-full grid-cols-[50px_1fr_50px] items-center gap-4 fill-inherit! text-inherit no-underline"}
    data-umami-event={`Social Button Clicked: ${type}`}
  >
    <SocialIcon name={icon} className="ml-2 size-6! fill-inherit" />
    <span class="w-full">
      <slot />
    </span>
    <ArrowSquareOutIcon className="size-6! justify-self-center fill-inherit" />
  </a>
</Button>
17create src/components/icons/social-icons.tsx
src/components/icons/social-icons.tsx
import { 
  DiscordLogoIcon, GithubLogoIcon, InstagramLogoIcon, LinkedinLogoIcon, ThreadsLogoIcon, TwitchLogoIcon, XLogoIcon, YoutubeLogoIcon,
EnvelopeIcon } from "@phosphor-icons/react";

const socialIcons = {
  twitter: XLogoIcon,
  github: GithubLogoIcon,
  linkedin: LinkedinLogoIcon,
  youtube: YoutubeLogoIcon,
  twitch: TwitchLogoIcon,
  instagram: InstagramLogoIcon,
  threads: ThreadsLogoIcon,
  discord: DiscordLogoIcon,
  email: EnvelopeIcon,
}

export type SocialIcons = keyof typeof socialIcons

export const SocialIcon = ({ name, className = "size-12" }: { name: keyof typeof socialIcons, className?: string }) => {
  const ResolvedSocialIcon = socialIcons[name]

  return <ResolvedSocialIcon  className={className} />
}
18create src/components/typography/heading.astro
src/components/typography/heading.astro
---
const { title, image = "https://placehold.co/150x150?text=+" } = Astro.props
---

<div class="mb-8 flex flex-col items-center justify-between gap-4">
  {
    image && (
      <img src={image.src} alt={image.alt} class="m-0! size-36 rounded-full" />
    )
  }
  <h1 class="m-0! text-right text-3xl font-semibold text-foreground">
    {title}
  </h1>
</div>
19create src/components/ui/button.tsx
src/components/ui/button.tsx
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import * as React from "react"

import { cn } from "@/lib/utils"

const buttonVariants = cva(
  "inline-flex items-center justify-center whitespace-nowrap rounded-base text-sm font-base ring-offset-white transition-all gap-2 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-black focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default:
          "text-main-foreground bg-main border-2 border-border shadow-shadow hover:translate-x-boxShadowX hover:translate-y-boxShadowY hover:shadow-none",
        noShadow: "text-main-foreground bg-main border-2 border-border",
        neutral:
          "bg-secondary-background text-foreground border-2 border-border shadow-shadow hover:translate-x-boxShadowX hover:translate-y-boxShadowY hover:shadow-none",
        reverse:
          "text-main-foreground bg-main border-2 border-border hover:translate-x-reverseBoxShadowX hover:translate-y-reverseBoxShadowY hover:shadow-shadow",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 px-3",
        lg: "h-11 px-8",
        icon: "size-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  },
)

function Button({
  className,
  variant,
  size,
  asChild = false,
  ...props
}: React.ComponentProps<"button"> &
  VariantProps<typeof buttonVariants> & {
    asChild?: boolean
  }) {
  const Comp = asChild ? Slot : "button"

  return (
    <Comp
      data-slot="button"
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  )
}

export { Button, buttonVariants }
20create src/components/ui/dropdown-menu.tsx
src/components/ui/dropdown-menu.tsx
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, CaretRightIcon, CircleIcon } from "@phosphor-icons/react"

import * as React from "react"

import { cn } from "@/lib/utils"

function DropdownMenu({
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
  return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}

function DropdownMenuTrigger({
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
  return (
    <DropdownMenuPrimitive.Trigger
      data-slot="dropdown-menu-trigger"
      {...props}
    />
  )
}

function DropdownMenuGroup({
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
  return <DropdownMenuPrimitive.Group {...props} />
}

function DropdownMenuPortal({
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
  return <DropdownMenuPrimitive.Portal {...props} />
}

function DropdownMenuSub({
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
  return <DropdownMenuPrimitive.Sub {...props} />
}

function DropdownMenuRadioGroup({
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
  return <DropdownMenuPrimitive.RadioGroup {...props} />
}

function DropdownMenuSubTrigger({
  className,
  inset,
  children,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
  inset?: boolean
}) {
  return (
    <DropdownMenuPrimitive.SubTrigger
      data-slot="dropdown-menu-sub-trigger"
      data-inset={inset}
      className={cn(
        "flex cursor-default select-none items-center rounded-base border-2 border-transparent bg-main px-2 py-1.5 text-sm font-base outline-hidden focus:border-border gap-2 data-[inset=true]:pl-8 [&_svg]:pointer-events-none [&_svg]:w-4 [&_svg]:h-4 [&_svg]:shrink-0",
        className,
      )}
      {...props}
    >
      {children}
      <CaretRightIcon className="ml-auto" />
    </DropdownMenuPrimitive.SubTrigger>
  )
}

function DropdownMenuSubContent({
  className,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
  return (
    <DropdownMenuPrimitive.SubContent
      className={cn(
        "z-50 min-w-[8rem] overflow-hidden rounded-base border-2 border-border bg-main p-1 font-base text-main-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)",
        className,
      )}
      {...props}
    />
  )
}

function DropdownMenuContent({
  className,
  sideOffset = 4,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
  return (
    <DropdownMenuPrimitive.Portal>
      <DropdownMenuPrimitive.Content
        data-slot="dropdown-menu-content"
        sideOffset={sideOffset}
        className={cn(
          "z-50 min-w-[8rem] overflow-hidden rounded-base border-2 border-border bg-main p-1 font-base text-main-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)",
          className,
        )}
        {...props}
      />
    </DropdownMenuPrimitive.Portal>
  )
}

function DropdownMenuItem({
  className,
  inset,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
  inset?: boolean
}) {
  return (
    <DropdownMenuPrimitive.Item
      data-slot="dropdown-menu-item"
      data-inset={inset}
      className={cn(
        "relative gap-2 [&_svg]:pointer-events-none [&_svg]:w-4 [&_svg]:h-4 [&_svg]:shrink-0 flex cursor-default select-none items-center rounded-base border-2 border-transparent data-[inset=true]:pl-8 bg-main px-2 py-1.5 text-sm font-base outline-hidden transition-colors focus:border-border data-disabled:pointer-events-none data-disabled:opacity-50",
        className,
      )}
      {...props}
    />
  )
}

function DropdownMenuCheckboxItem({
  className,
  children,
  checked,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
  return (
    <DropdownMenuPrimitive.CheckboxItem
      className={cn(
        "relative flex cursor-default select-none items-center rounded-base border-2 border-transparent gap-2 py-1.5 pl-8 pr-2 text-sm font-base text-main-foreground outline-hidden transition-colors focus:border-border data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      checked={checked}
      {...props}
    >
      <span className="absolute left-2 flex size-3.5 items-center justify-center">
        <DropdownMenuPrimitive.ItemIndicator>
          <CheckIcon />
        </DropdownMenuPrimitive.ItemIndicator>
      </span>
      {children}
    </DropdownMenuPrimitive.CheckboxItem>
  )
}

function DropdownMenuRadioItem({
  className,
  children,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
  return (
    <DropdownMenuPrimitive.RadioItem
      className={cn(
        "relative flex cursor-default select-none items-center rounded-base border-2 border-transparent gap-2 py-1.5 pl-8 pr-2 text-sm font-base outline-hidden transition-colors focus:border-border data-disabled:pointer-events-none data-disabled:opacity-50",
        className,
      )}
      {...props}
    >
      <span className="absolute left-2 flex size-3.5 items-center justify-center">
        <DropdownMenuPrimitive.ItemIndicator>
          <CircleIcon className="size-2 fill-current" />
        </DropdownMenuPrimitive.ItemIndicator>
      </span>
      {children}
    </DropdownMenuPrimitive.RadioItem>
  )
}

function DropdownMenuLabel({
  className,
  inset,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
  inset?: boolean
}) {
  return (
    <DropdownMenuPrimitive.Label
      data-slot="dropdown-menu-label"
      data-inset={inset}
      className={cn(
        "px-2 py-1.5 text-sm font-heading data-[inset]:pl-8",
        className,
      )}
      {...props}
    />
  )
}

function DropdownMenuSeparator({
  className,
  ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
  return (
    <DropdownMenuPrimitive.Separator
      className={cn("-mx-1 my-1 h-0.5 bg-border", className)}
      {...props}
    />
  )
}

function DropdownMenuShortcut({
  className,
  ...props
}: React.HTMLAttributes<HTMLSpanElement>) {
  return (
    <span
      data-slot="dropdown-menu-shortcut"
      className={cn("ml-auto text-xs font-base tracking-widest", className)}
      {...props}
    />
  )
}

export {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuCheckboxItem,
  DropdownMenuRadioItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuGroup,
  DropdownMenuPortal,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuRadioGroup,
}
21create src/components/ui/separator.tsx
src/components/ui/separator.tsx
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"

import { cn } from "@/lib/utils"

function Separator({
  className,
  orientation = "horizontal",
  ...props
}: SeparatorPrimitive.Props) {
  return (
    <SeparatorPrimitive
      data-slot="separator"
      orientation={orientation}
      className={cn(
        "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
        className
      )}
      {...props}
    />
  )
}

export { Separator }
22dynamic-create src/consts.ts
src/consts.ts
export default function (_variables: Record<string, string>): string {
  return `export const SITE_TITLE = \`${_variables.name.replace("`", "\\`")}\`;
export const SITE_DESCRIPTION = \`${_variables.siteDescription.replace("`", "\\`")}\`;
`;
}
23create src/content.config.ts
src/content.config.ts
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const page = defineCollection({
	// Load Markdown and MDX files in the `src/content/pages/` directory.
	loader: glob({ base: './src/content/pages', pattern: '**/*.{md,mdx}' }),
	// Type-check frontmatter using a schema
	schema: () =>
		z.object({
			title: z.string(),
			description: z.string(),
			// Transform string to Date object
			updatedDate: z.coerce.date().optional(),
		}),
});

export const collections = { page };
24create src/hooks/use-mobile.ts
src/hooks/use-mobile.ts
import * as React from "react"

const MOBILE_BREAKPOINT = 768

export function useIsMobile() {
  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)

  React.useEffect(() => {
    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
    const onChange = () => {
      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    }
    mql.addEventListener("change", onChange)
    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    return () => mql.removeEventListener("change", onChange)
  }, [])

  return !!isMobile
}
25create src/layouts/main.astro
src/layouts/main.astro
\n\n\n \n \n \n \n
\n
\n \n
\n \n
\n \n
\n
\n
\n
\n \n\n"
---
import "@/styles/global.css"
import BaseHead from "@/components/BaseHead.astro"
import { SITE_DESCRIPTION, SITE_TITLE } from "@/consts"
import { ModeToggle } from "@/components/ModeToggle"
import { Separator } from "@/components/ui/separator"
---

<script is:inline>
  const getThemePreference = () => {
    if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
      return localStorage.getItem("theme")
    }
    return window.matchMedia("(prefers-color-scheme: dark)").matches
      ? "dark"
      : "light"
  }
  const isDark = getThemePreference() === "dark"
  document.documentElement.classList[isDark ? "add" : "remove"]("dark")

  if (typeof localStorage !== "undefined") {
    const observer = new MutationObserver(() => {
      const isDark = document.documentElement.classList.contains("dark")
      localStorage.setItem("theme", isDark ? "dark" : "light")
    })
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"],
    })
  }
</script>

<html lang="en">
  <head>
    <BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
  </head>
  <body>
    <div class="flex min-h-svh w-screen flex-col items-center p-8">
      <main class="prose w-full md:w-md">
        <slot />
        <div class="mt-6 flex flex-col gap-4">
          <Separator />
          <div class="flex justify-end">
            <ModeToggle client:load />
          </div>
        </div>
      </main>
    </div>
  </body>
</html>
26create src/lib/utils.ts
src/lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
27dynamic-create src/pages/index.astro
src/pages/index.astro
export default function (_variables: Record<string, string>): string {
  const DEFAULT_PROFILE_IMAGE = "https://placehold.co/150x150?text=+";

  const socials: Array<[string, string, string]> = [];

  if (_variables.twitterHandle) {
    socials.push(["twitter", _variables.twitterHandle, `Twitter @${_variables.twitterHandle}`]);
  }

  if (_variables.linkedinHandle) {
    socials.push(["linkedin", _variables.linkedinHandle, "LinkedIn"]);
  }

  if (_variables.youtubeHandle) {
    socials.push(["youtube", _variables.youtubeHandle, `Youtube @${_variables.youtubeHandle}`]);
  }

  if (_variables.githubHandle) {
    socials.push(["github", _variables.githubHandle, `Github @${_variables.githubHandle}`]);
  }

  if (_variables.email) {
    socials.push(["email", _variables.email, "Email me"]);
  }

  const multiline = new Set(["twitter", "youtube"]);
  const buttons = socials
    .map(([type, handle, label]) =>
      multiline.has(type)
        ? `    <SocialButton type="${type}" handle="${handle}"\n      >${label}</SocialButton\n    >`
        : `    <SocialButton type="${type}" handle="${handle}">${label}</SocialButton>`,
    )
    .join("\n\n");

  const imageProp = `\n    image={{ src: "${_variables.profileImage || DEFAULT_PROFILE_IMAGE}", alt: "Profile Picture" }}`

  return `---
import Layout from "@/layouts/main.astro"
import Heading from "@/components/typography/heading.astro"
import SocialButton from "@/components/buttons/social-button.astro"
---

<Layout>
  <Heading
    title="${_variables.name}"${imageProp}
  />
${buttons ? `\n  <div class="flex flex-col gap-4">\n${buttons}\n  </div>\n` : ""}</Layout>
`;
}
28create src/styles/global.css
src/styles/global.css
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@fontsource-variable/dm-sans";
@import "@fontsource-variable/inter";
@plugin "@tailwindcss/typography";

@custom-variant dark (&:is(.dark *));

:root {
  --background: oklch(94.27% 0.0268 242.57);
  --secondary-background: oklch(100% 0 0);
  --foreground: oklch(0% 0 0);
  --main-foreground: oklch(0% 0 0);
  --main: oklch(66.9% 0.18368 248.8066);
  --border: oklch(0% 0 0);
  --ring: oklch(0% 0 0);
  --overlay: oklch(0% 0 0 / 0.8);
  --shadow: 4px 4px 0px 0px var(--border);
  --chart-1: #0099FF;
  --chart-2: #FF4D50;
  --chart-3: #FACC00;
  --chart-4: #05E17A;
  --chart-5: #7A83FF;
  --chart-active-dot: #000;
}

.dark {
  --background: oklch(27.08% 0.0336 240.69);
  --secondary-background: oklch(23.93% 0 0);
  --foreground: oklch(92.49% 0 0);
  --main-foreground: oklch(0% 0 0);
  --main: oklch(61.9% 0.16907 248.5982);
  --border: oklch(0% 0 0);
  --ring: oklch(100% 0 0);
  --shadow: 4px 4px 0px 0px var(--border);
  --chart-1: #008AE5;
  --chart-2: #FF6669;
  --chart-3: #E0B700;
  --chart-4: #04C86D;
  --chart-5: #7A83FF;
  --chart-active-dot: #fff;
}

@theme inline {
  --color-main: var(--main);
  --color-background: var(--background);
  --color-secondary-background: var(--secondary-background);
  --color-foreground: var(--foreground);
  --color-main-foreground: var(--main-foreground);
  --color-border: var(--border);
  --color-overlay: var(--overlay);
  --color-ring: var(--ring);
  --color-chart-1: var(--chart-1);
  --color-chart-2: var(--chart-2);
  --color-chart-3: var(--chart-3);
  --color-chart-4: var(--chart-4);
  --color-chart-5: var(--chart-5);

  --spacing-boxShadowX: 4px;
  --spacing-boxShadowY: 4px;
  --spacing-reverseBoxShadowX: -4px;
  --spacing-reverseBoxShadowY: -4px;
  --radius-base: 5px;
  --shadow-shadow: var(--shadow);
  --font-weight-base: 500;
  --font-weight-heading: 700;
}
  
@layer base {
  body {
    @apply text-foreground font-base bg-background;
  }

  h1, h2, h3, h4, h5, h6{
    @apply font-heading;
  }
}
29create tsconfig.json
tsconfig.json
{
  "extends": "astro/tsconfigs/strict",
  "include": [
    ".astro/types.d.ts",
    "**/*",
    "./worker-configuration.d.ts"
  ],
  "exclude": [
    "dist"
  ],
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "react",
    "paths": {
      "@/*": [
        "./src/*"
      ]
    }
  }
}