├── .dockerignore ├── .gitignore ├── .vscode ├── extensions.json └── launch.json ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── astro.config.mjs ├── docker-compose.yml ├── package-lock.json ├── package.json ├── public ├── demo-1.jpg ├── demo-2.jpg ├── favicon.svg ├── fonts │ └── InterVariable.woff2 └── logo.webp ├── remark-reading-time.mjs ├── src ├── components │ ├── Link.astro │ ├── ProjectCard.astro │ ├── ResumeItem.astro │ ├── SocialLink.astro │ └── SocialLinkBox.astro ├── config.ts ├── content.config.ts ├── content │ └── posts │ │ ├── Test 2.md │ │ └── Test.md ├── env.d.ts ├── layouts │ └── Layout.astro ├── pages │ ├── about.astro │ ├── blog │ │ ├── [id].astro │ │ └── index.astro │ ├── index.astro │ └── projects.astro └── types │ └── config.ts ├── tailwind.config.mjs └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine AS runtime 2 | WORKDIR /app 3 | 4 | COPY . . 5 | 6 | RUN npm install 7 | RUN npm run build 8 | 9 | ENV HOST=0.0.0.0 10 | ENV PORT=4321 11 | EXPOSE 4321 12 | CMD node ./dist/server/entry.mjs 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tim Witzdam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Minimal portfolio template for Astro

3 |

Open source minimalistic portfolio template created with Astro and Tailwind. Design inspired by Brian Ruiz

4 | 5 | Theme preview 6 | 7 |
8 |
9 | 10 | [Preview](https://minimal-portfolio.witzdam.com/) 11 | 12 | ## 🔥 Features 13 | 14 | - Fully responsive 15 | - Perfect Google Lighthouse score 16 | - Fast and SEO optimized blog with Markdown support 17 | - Fully customizable 18 | - Home, About, Projects and Blog pages 19 | - Self host ready with Docker Compose 20 | 21 | ## 🚀 Getting started 22 | 23 | 1. Clone/download the repo 24 | 25 | ``` 26 | git clone https://github.com/TimWitzdam/astro-minimal-portfolio-template.git 27 | cd astro-minimal-portfolio-template 28 | ``` 29 | 30 | 2. Install dependencies 31 | 32 | ``` 33 | npm i 34 | ``` 35 | 36 | 3. Run the development server 37 | 38 | ``` 39 | npm run dev 40 | ``` 41 | 42 | 4. [Configure](#🔧-configuration) your instance 43 | 44 | ## 🧞 Commands 45 | 46 | All commands are run from the root of the project, from a terminal: 47 | 48 | | Command | Action | 49 | | :---------------- | :------------------------------------------- | 50 | | `npm install` | Installs dependencies | 51 | | `npm run dev` | Starts local dev server at `localhost:4321` | 52 | | `npm run build` | Build your production site to `./dist/` | 53 | | `npm run preview` | Preview your build locally, before deploying | 54 | 55 | ## 🔧 Configuration 56 | 57 | This is the default example config file, which allows you to change pretty much anything content related on the site. 58 | 59 | ```JS 60 | // src/config.ts 61 | import type { 62 | NavBarLink, 63 | SocialLink, 64 | Identity, 65 | AboutPageContent, 66 | ProjectPageContent, 67 | BlogPageContent, 68 | HomePageContent, 69 | } from "./types/config"; 70 | 71 | export const identity: Identity = { 72 | name: "Tim Witzdam", 73 | logo: "/logo.webp", 74 | email: "tim@witzdam.com", 75 | }; 76 | 77 | export const navBarLinks: NavBarLink[] = [ 78 | { 79 | title: "Home", 80 | url: "/", 81 | }, 82 | { 83 | title: "About", 84 | url: "/about", 85 | }, 86 | { 87 | title: "Projects", 88 | url: "/projects", 89 | }, 90 | { 91 | title: "Blog", 92 | url: "/blog", 93 | }, 94 | ]; 95 | 96 | export const socialLinks: SocialLink[] = [ 97 | { 98 | title: "GitHub", 99 | url: "https://github.com/TimWitzdam", 100 | icon: "mdi:github", 101 | external: true, 102 | }, 103 | { 104 | title: "Mail", 105 | url: "mailto:tim@witzdam.com", 106 | icon: "mdi:email", 107 | }, 108 | ]; 109 | 110 | // Home (/) 111 | export const homePageContent: HomePageContent = { 112 | seo: { 113 | title: "Tim Witzdam", 114 | description: 115 | "Full time student from Germany who loves building cool things using code.", 116 | image: identity.logo, 117 | }, 118 | role: "Student & Software Developer", 119 | description: 120 | "I'm Tim Witzdam, a full time student from Germany who also loves building cool things using code.", 121 | socialLinks: socialLinks, 122 | links: [ 123 | { 124 | title: "My Projects", 125 | url: "/projects", 126 | }, 127 | { 128 | title: "About Me", 129 | url: "/about", 130 | }, 131 | ], 132 | }; 133 | 134 | // About (/about) 135 | export const aboutPageContent: AboutPageContent = { 136 | seo: { 137 | title: "About | Tim Witzdam", 138 | description: 139 | "Full time student from Germany who loves building cool things using code.", 140 | image: identity.logo, 141 | }, 142 | subtitle: "Some information about myself", 143 | about: { 144 | description: ` 145 | I'm Tim Witzdam, a full time student from Germany who also loves building cool things using code. 146 |

147 | Lorem ipsum dolor sit amet consectetur, adipisicing elit. Eaque placeat est architecto tempora voluptatem sit suscipit aspernatur?

148 | Facere quibusdam reiciendis, distinctio sunt praesentium error accusantium consectetur nemo vero officia itaque.`, // Markdown is supported 149 | image_l: { 150 | url: "/demo-1.jpg", 151 | alt: "Left Picture", 152 | }, 153 | image_r: { 154 | url: "/demo-1.jpg", 155 | alt: "Right Picture", 156 | }, 157 | }, 158 | work: { 159 | description: `I've worked with a variety of technologies and tools to build cool things. Here are some of the projects I've worked on.`, // Markdown is supported 160 | items: [ 161 | { 162 | title: "Software Developer", 163 | company: { 164 | name: "Freelance", 165 | image: "/logo.webp", 166 | url: "https://github.com/TimWitzdam", 167 | }, 168 | date: "2021 - Present", 169 | }, 170 | { 171 | title: "Software Developer", 172 | company: { 173 | name: "Freelance", 174 | image: "/logo.webp", 175 | url: "https://github.com/TimWitzdam", 176 | }, 177 | date: "2019 - 2021", 178 | }, 179 | ], 180 | }, 181 | connect: { 182 | description: `I'm always interested in meeting new people and learning new things. Feel free to connect with me on any of the following platforms.`, // Markdown is supported 183 | links: socialLinks, 184 | }, 185 | }; 186 | 187 | // Projects (/projects) 188 | export const projectsPageContent: ProjectPageContent = { 189 | seo: { 190 | title: "Projects | Tim Witzdam", 191 | description: "Check out what I've been working on.", 192 | image: identity.logo, 193 | }, 194 | subtitle: "Check out what I've been working on.", 195 | projects: [ 196 | { 197 | title: "Project 1", 198 | description: "Project 1 Description", 199 | image: "/demo-2.jpg", 200 | year: "2024", 201 | url: "https://github.com/TimWitzdam", 202 | }, 203 | { 204 | title: "Project 1", 205 | description: "Project 1 Description", 206 | image: "/demo-2.jpg", 207 | year: "2024", 208 | url: "https://github.com/TimWitzdam", 209 | }, 210 | { 211 | title: "Project 1", 212 | description: "Project 1 Description", 213 | image: "/demo-2.jpg", 214 | year: "2024", 215 | url: "https://github.com/TimWitzdam", 216 | }, 217 | ], 218 | }; 219 | 220 | // Blog (/blog) 221 | export const blogPageContent: BlogPageContent = { 222 | seo: { 223 | title: "Blog | Tim Witzdam", 224 | description: "Thoughts, stories and ideas.", 225 | image: identity.logo, 226 | }, 227 | subtitle: "Thoughts, stories and ideas.", 228 | }; 229 | ``` 230 | 231 | ## 👀 Any questions or problems? 232 | 233 | Feel free to open an issue or even contribute by fixing a problem. 234 | 235 | I'm also available via mail: [contact@witzdam.com](mailto:contact@witzdam.com) 236 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import tailwind from "@astrojs/tailwind"; 3 | import icon from "astro-icon"; 4 | import { remarkReadingTime } from "./remark-reading-time.mjs"; 5 | import node from "@astrojs/node"; 6 | 7 | // https://astro.build/config 8 | export default defineConfig({ 9 | integrations: [tailwind(), icon()], 10 | output: "server", 11 | adapter: node({ 12 | mode: "standalone", 13 | }), 14 | markdown: { 15 | remarkPlugins: [remarkReadingTime], 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | minimal-portforio-template: 3 | container_name: minimal-portforio-template 4 | image: minimal-portforio-template 5 | build: 6 | context: ./ 7 | dockerfile: Dockerfile 8 | target: runtime 9 | ports: 10 | - "4321:4321" 11 | restart: always 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimal-portfolio", 3 | "type": "module", 4 | "version": "1.0.6", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro check && astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/check": "^0.9.4", 14 | "@astrojs/node": "^9.0.0", 15 | "@astrojs/tailwind": "^5.1.4", 16 | "astro": "^5.1.1", 17 | "astro-icon": "^1.1.0", 18 | "marked": "^15.0.4", 19 | "mdast-util-to-string": "^4.0.0", 20 | "reading-time": "^1.5.0", 21 | "tailwindcss": "^3.4.1", 22 | "typescript": "^5.4.3" 23 | }, 24 | "devDependencies": { 25 | "@iconify-json/mdi": "^1.1.64", 26 | "@tailwindcss/typography": "^0.5.12" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/demo-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimWitzdam/astro-minimal-portfolio-template/be1bddcedbb6bc2f3725aa0b20b2fc74fe737078/public/demo-1.jpg -------------------------------------------------------------------------------- /public/demo-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimWitzdam/astro-minimal-portfolio-template/be1bddcedbb6bc2f3725aa0b20b2fc74fe737078/public/demo-2.jpg -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /public/fonts/InterVariable.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimWitzdam/astro-minimal-portfolio-template/be1bddcedbb6bc2f3725aa0b20b2fc74fe737078/public/fonts/InterVariable.woff2 -------------------------------------------------------------------------------- /public/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimWitzdam/astro-minimal-portfolio-template/be1bddcedbb6bc2f3725aa0b20b2fc74fe737078/public/logo.webp -------------------------------------------------------------------------------- /remark-reading-time.mjs: -------------------------------------------------------------------------------- 1 | import getReadingTime from "reading-time"; 2 | import { toString } from "mdast-util-to-string"; 3 | 4 | export function remarkReadingTime() { 5 | return function (tree, { data }) { 6 | const textOnPage = toString(tree); 7 | const readingTime = getReadingTime(textOnPage); 8 | // readingTime.text will give us minutes read as a friendly string, 9 | // i.e. "3 min read" 10 | data.astro.frontmatter.minutesRead = readingTime.text; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Link.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from "astro-icon/components"; 3 | interface Props { 4 | text: string; 5 | href: string; 6 | external?: boolean; 7 | noAnchor?: boolean; 8 | } 9 | 10 | const { text, href, external, noAnchor } = Astro.props; 11 | --- 12 | 13 | { 14 | !noAnchor ? ( 15 | 20 |
21 | 22 |
23 | {text} 24 |
25 | ) : ( 26 |
27 |
28 | 29 |
30 | {text} 31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/components/ProjectCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Image } from "astro:assets"; 3 | import type { Project } from "../types/config"; 4 | import {marked} from "marked" 5 | 6 | type Props = Project; 7 | 8 | const { title, description, image, year, url } = Astro.props; 9 | 10 | const htmlDescription = marked.parse(description) 11 | --- 12 | 13 | 18 |
19 | {title} 26 |
27 |
28 |
29 |

{title}

30 | · {year} 31 |
32 |
33 | 34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /src/components/ResumeItem.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Image } from "astro:assets"; 3 | import type { ResumeItem } from "../types/config"; 4 | 5 | type Props = ResumeItem; 6 | 7 | const { title, company, date } = Astro.props; 8 | --- 9 | 10 | 14 |
15 | {company.name} 22 |
23 |

{title}

24 |

{company.name}

25 |
26 |
27 |

{date}

28 |
29 | -------------------------------------------------------------------------------- /src/components/SocialLink.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from "astro-icon/components"; 3 | import type { SocialLink } from "../types/config"; 4 | 5 | type Props = SocialLink; 6 | 7 | const { title, url, icon, external } = Astro.props; 8 | --- 9 | 10 | 15 | 16 |
17 |

{title}

18 |
19 |
20 | -------------------------------------------------------------------------------- /src/components/SocialLinkBox.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from "astro-icon/components"; 3 | import type { SocialLink } from "../types/config"; 4 | 5 | type Props = SocialLink; 6 | 7 | const { title, url, icon, external } = Astro.props; 8 | --- 9 | 10 | 15 | 16 |
17 |

{title}

18 |
19 | 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | NavBarLink, 3 | SocialLink, 4 | Identity, 5 | AboutPageContent, 6 | ProjectPageContent, 7 | BlogPageContent, 8 | HomePageContent, 9 | } from "./types/config"; 10 | 11 | export const identity: Identity = { 12 | name: "Tim Witzdam", 13 | logo: "/logo.webp", 14 | email: "tim@witzdam.com", 15 | }; 16 | 17 | export const navBarLinks: NavBarLink[] = [ 18 | { 19 | title: "Home", 20 | url: "/", 21 | }, 22 | { 23 | title: "About", 24 | url: "/about", 25 | }, 26 | { 27 | title: "Projects", 28 | url: "/projects", 29 | }, 30 | { 31 | title: "Blog", 32 | url: "/blog", 33 | }, 34 | ]; 35 | 36 | export const socialLinks: SocialLink[] = [ 37 | { 38 | title: "GitHub", 39 | url: "https://github.com/TimWitzdam", 40 | icon: "mdi:github", 41 | external: true, 42 | }, 43 | { 44 | title: "Mail", 45 | url: "mailto:tim@witzdam.com", 46 | icon: "mdi:email", 47 | }, 48 | ]; 49 | 50 | // Home (/) 51 | export const homePageContent: HomePageContent = { 52 | seo: { 53 | title: "Tim Witzdam", 54 | description: 55 | "Full time student from Germany who loves building cool things using code.", 56 | image: identity.logo, 57 | }, 58 | role: "Student & Software Developer", 59 | description: 60 | "I'm Tim Witzdam, a full time student from Germany who also loves building cool things using code.", 61 | socialLinks: socialLinks, 62 | links: [ 63 | { 64 | title: "My Projects", 65 | url: "/projects", 66 | }, 67 | { 68 | title: "About Me", 69 | url: "/about", 70 | }, 71 | ], 72 | }; 73 | 74 | // About (/about) 75 | export const aboutPageContent: AboutPageContent = { 76 | seo: { 77 | title: "About | Tim Witzdam", 78 | description: 79 | "Full time student from Germany who loves building cool things using code.", 80 | image: identity.logo, 81 | }, 82 | subtitle: "Some information about myself", 83 | about: { 84 | description: ` 85 | I'm Tim Witzdam, a full time student from Germany who also loves building cool things using code. 86 |

87 | Lorem ipsum dolor sit amet consectetur, adipisicing elit. Eaque placeat est architecto tempora voluptatem sit suscipit aspernatur?

88 | Facere quibusdam reiciendis, distinctio sunt praesentium error accusantium consectetur nemo vero officia itaque.`, // Markdown is supported 89 | image_l: { 90 | url: "/demo-1.jpg", 91 | alt: "Left Picture", 92 | }, 93 | image_r: { 94 | url: "/demo-1.jpg", 95 | alt: "Right Picture", 96 | }, 97 | }, 98 | work: { 99 | description: `I've worked with a variety of technologies and tools to build cool things. Here are some of the projects I've worked on.`, // Markdown is supported 100 | items: [ 101 | { 102 | title: "Software Developer", 103 | company: { 104 | name: "Freelance", 105 | image: "/logo.webp", 106 | url: "https://github.com/TimWitzdam", 107 | }, 108 | date: "2021 - Present", 109 | }, 110 | { 111 | title: "Software Developer", 112 | company: { 113 | name: "Freelance", 114 | image: "/logo.webp", 115 | url: "https://github.com/TimWitzdam", 116 | }, 117 | date: "2019 - 2021", 118 | }, 119 | ], 120 | }, 121 | connect: { 122 | description: `I'm always interested in meeting new people and learning new things. Feel free to connect with me on any of the following platforms.`, // Markdown is supported 123 | links: socialLinks, 124 | }, 125 | }; 126 | 127 | // Projects (/projects) 128 | export const projectsPageContent: ProjectPageContent = { 129 | seo: { 130 | title: "Projects | Tim Witzdam", 131 | description: "Check out what I've been working on.", 132 | image: identity.logo, 133 | }, 134 | subtitle: "Check out what I've been working on.", 135 | projects: [ 136 | { 137 | title: "Project 1", 138 | description: "Project 1 Description", 139 | image: "/demo-2.jpg", 140 | year: "2024", 141 | url: "https://github.com/TimWitzdam", 142 | }, 143 | { 144 | title: "Project 1", 145 | description: "Project 1 Description", 146 | image: "/demo-2.jpg", 147 | year: "2024", 148 | url: "https://github.com/TimWitzdam", 149 | }, 150 | { 151 | title: "Project 1", 152 | description: "Project 1 Description", 153 | image: "/demo-2.jpg", 154 | year: "2024", 155 | url: "https://github.com/TimWitzdam", 156 | }, 157 | ], 158 | }; 159 | 160 | // Blog (/blog) 161 | export const blogPageContent: BlogPageContent = { 162 | seo: { 163 | title: "Blog | Tim Witzdam", 164 | description: "Thoughts, stories and ideas.", 165 | image: identity.logo, 166 | }, 167 | subtitle: "Thoughts, stories and ideas.", 168 | }; 169 | -------------------------------------------------------------------------------- /src/content.config.ts: -------------------------------------------------------------------------------- 1 | // Import utilities from `astro:content` 2 | import { z, defineCollection } from "astro:content"; 3 | import { glob } from "astro/loaders"; 4 | 5 | // Define a `type` and `schema` for each collection 6 | const postsCollection = defineCollection({ 7 | loader: glob({ pattern: "**/[^_]*.{md,mdx}", base: "./src/content/posts" }), 8 | schema: z.object({ 9 | title: z.string(), 10 | pubDate: z.date(), 11 | description: z.string(), 12 | author: z.string(), 13 | image: z.object({ 14 | url: z.string(), 15 | alt: z.string(), 16 | }), 17 | readingTime: z.number().optional(), 18 | }), 19 | }); 20 | // Export a single `collections` object to register your collection(s) 21 | export const collections = { 22 | posts: postsCollection, 23 | }; 24 | -------------------------------------------------------------------------------- /src/content/posts/Test 2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "My First Blog Post with a very long name" 3 | pubDate: 2022-07-01 #Y-M-D 4 | description: "Test" 5 | author: "Tim" 6 | image: { url: "/demo-1.jpg", alt: "Test" } 7 | --- 8 | 9 | This is test content 10 | -------------------------------------------------------------------------------- /src/content/posts/Test.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "My First Blog Post with a very long name" 3 | pubDate: 2022-07-01 #Y-M-D 4 | description: "Test" 5 | author: "Tim" 6 | image: { url: "/logo.webp", alt: "Test" } 7 | --- 8 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Image } from "astro:assets"; 3 | import { Icon } from "astro-icon/components"; 4 | import { navBarLinks, identity } from "../config"; 5 | import type { SEOInfo } from "../types/config"; 6 | 7 | interface Props { 8 | seo: SEOInfo; 9 | } 10 | 11 | const { seo } = Astro.props; 12 | --- 13 | 14 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {seo.title} 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 |
67 | 68 | {"Logo"} 75 | 76 | 120 |
121 | 126 | 127 | 128 |
129 |
130 |
131 |
132 | 133 |
134 |
135 | 136 | 137 | 138 | 176 | -------------------------------------------------------------------------------- /src/pages/about.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Image } from "astro:assets"; 3 | import Layout from "../layouts/Layout.astro"; 4 | import { aboutPageContent } from "../config"; 5 | import ResumeItem from "../components/ResumeItem.astro"; 6 | import SocialLinkBox from "../components/SocialLinkBox.astro"; 7 | import {marked} from "marked" 8 | 9 | const pageDescription = marked.parse(aboutPageContent.about.description) 10 | const workDescription = marked.parse(aboutPageContent.work.description) 11 | const connectDescription = marked.parse(aboutPageContent.connect.description) 12 | --- 13 | 14 | 15 |
16 |

About

17 |

{aboutPageContent.subtitle}

18 |
19 | 20 |
21 |
22 | {aboutPageContent.about.image_l.alt} 29 | {aboutPageContent.about.image_r.alt} 36 |
37 |
38 |
39 |

Work

40 |
41 |
42 | 43 |
44 |
45 | {aboutPageContent.work.items.map((item) => )} 46 |
47 |
48 |
49 |
50 |

Connect

51 |
52 |
53 | 54 |
55 |
56 | { 57 | aboutPageContent.connect.links.map((item) => ( 58 | 59 | )) 60 | } 61 |
62 |
63 |
64 |
65 | -------------------------------------------------------------------------------- /src/pages/blog/[id].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../layouts/Layout.astro"; 3 | import { getEntry, render } from "astro:content"; 4 | import { Image } from "astro:assets"; 5 | import { identity } from "../../config"; 6 | 7 | const { id } = Astro.params; 8 | const entry = await getEntry("posts", id as string); 9 | 10 | if (!entry) { 11 | return Astro.redirect("/blog"); 12 | } 13 | 14 | const { Content, remarkPluginFrontmatter} = await render(entry); 15 | --- 16 | 17 | 24 |
25 |

{entry.data.title}

26 |

27 | {entry.data.description} 28 |

29 |
30 | {identity.name} 37 |
38 |

{identity.name}

39 |

40 | {entry.data.pubDate.toLocaleDateString()} · { 41 | remarkPluginFrontmatter.minutesRead 42 | } 43 |

44 |
45 |
46 | {entry.data.image.alt} 53 |
56 | 57 |
58 |
59 |
60 | -------------------------------------------------------------------------------- /src/pages/blog/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Image } from "astro:assets"; 3 | import Layout from "../../layouts/Layout.astro"; 4 | import { getCollection } from "astro:content"; 5 | import { blogPageContent } from "../../config"; 6 | import { render } from "astro:content"; 7 | 8 | const posts = await getCollection("posts"); 9 | posts.sort((a, b) => { 10 | const dateA = new Date(a.data.pubDate).getTime(); 11 | const dateB = new Date(b.data.pubDate).getTime(); 12 | return dateB - dateA; 13 | }); 14 | for (const post of posts) { 15 | const { remarkPluginFrontmatter } = await render(post); 16 | post.data.readingTime = remarkPluginFrontmatter.minutesRead; 17 | } 18 | --- 19 | 20 | 21 |
22 |

Blog

23 |

{blogPageContent.subtitle}

24 | 25 | 51 |
52 |
53 | -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Image } from "astro:assets"; 3 | import Layout from "../layouts/Layout.astro"; 4 | import { homePageContent, identity } from "../config"; 5 | import SocialLink from "../components/SocialLink.astro"; 6 | import Link from "../components/Link.astro"; 7 | import { getCollection } from "astro:content"; 8 | 9 | const posts = await getCollection("posts"); 10 | posts.sort((a, b) => { 11 | const dateA = new Date(a.data.pubDate).getTime(); 12 | const dateB = new Date(b.data.pubDate).getTime(); 13 | return dateB - dateA; 14 | }); 15 | posts.splice(2) 16 | --- 17 | 18 | 19 |
20 |

{identity.name}

21 |

{homePageContent.role}

22 |
23 | {identity.name} 30 |
31 | { 32 | homePageContent.socialLinks.map((link) => ( 33 | 39 | )) 40 | } 41 |
42 |
43 |

44 | {homePageContent.description} 45 |

46 |
47 | { 48 | homePageContent.links.map((link) => ( 49 | 50 | )) 51 | } 52 |
53 |
54 |
55 |

Latest Posts

56 | 81 |
82 | View all 87 |
88 |
89 |
90 | -------------------------------------------------------------------------------- /src/pages/projects.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../layouts/Layout.astro"; 3 | import { projectsPageContent } from "../config"; 4 | import ProjectCard from "../components/ProjectCard.astro"; 5 | --- 6 | 7 | 8 |
9 |

Projects

10 |

{projectsPageContent.subtitle}

11 |
12 | { 13 | projectsPageContent.projects.map((project) => ( 14 | 15 | )) 16 | } 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /src/types/config.ts: -------------------------------------------------------------------------------- 1 | export type NavBarLink = { 2 | title: string; 3 | url: string; 4 | external?: boolean; 5 | }; 6 | 7 | export type SocialLink = { 8 | title: string; 9 | url: string; 10 | icon: string; 11 | external?: boolean; 12 | }; 13 | 14 | export type Identity = { 15 | name: string; 16 | logo: string; 17 | email: string; 18 | }; 19 | 20 | export type SEOInfo = { 21 | title: string; 22 | description: string; 23 | image: string; 24 | }; 25 | 26 | export type HomePageContent = { 27 | seo: SEOInfo; 28 | role: string; 29 | description: string; 30 | socialLinks: SocialLink[]; 31 | links: { 32 | title: string; 33 | url: string; 34 | external?: boolean; 35 | }[]; 36 | }; 37 | 38 | export type ResumeItem = { 39 | title: string; 40 | company: { 41 | name: string; 42 | image: string; 43 | url: string; 44 | }; 45 | date: string; 46 | }; 47 | 48 | export type AboutPageContent = { 49 | seo: SEOInfo; 50 | subtitle: string; 51 | about: { 52 | description: string; 53 | image_l: { 54 | url: string; 55 | alt: string; 56 | }; 57 | image_r: { 58 | url: string; 59 | alt: string; 60 | }; 61 | }; 62 | work: { 63 | description: string; 64 | items: ResumeItem[]; 65 | }; 66 | connect: { 67 | description: string; 68 | links: SocialLink[]; 69 | }; 70 | }; 71 | 72 | export type Project = { 73 | title: string; 74 | description: string; 75 | image: string; 76 | year: string; 77 | url: string; 78 | }; 79 | 80 | export type ProjectPageContent = { 81 | seo: SEOInfo; 82 | subtitle: string; 83 | projects: Project[]; 84 | }; 85 | 86 | export type BlogPageContent = { 87 | seo: SEOInfo; 88 | subtitle: string; 89 | }; 90 | -------------------------------------------------------------------------------- /tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | "gray-bg": "#2E2E2E", 8 | }, 9 | }, 10 | }, 11 | plugins: [require("@tailwindcss/typography")], 12 | }; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "include": [".astro/types.d.ts", "**/*"], 4 | "exclude": ["dist"] 5 | } 6 | --------------------------------------------------------------------------------