From f39ff829e8c9b69ba741ab15d7e02f391233a640 Mon Sep 17 00:00:00 2001
From: Christian Cito <30470476+chrcit@users.noreply.github.com>
Date: Mon, 23 Dec 2024 13:49:01 +0100
Subject: [PATCH] refactor: wip remix port
---
.gitignore | 2 +
app/components/ErrorBoundary.tsx | 111 +
app/components/Footer.tsx | 32 +
app/components/Header.tsx | 74 +
app/components/Particles.tsx | 85 +
app/components/RelatedContent.tsx | 55 +
app/components/SocialIcon.tsx | 69 +
app/components/SocialLinks.tsx | 33 +
app/components/Toast.tsx | 113 +
app/components/embeds/EmbedOverlay.tsx | 57 +
app/components/embeds/InstagramEmbed.tsx | 83 +
app/components/embeds/RedditEmbed.tsx | 49 +
app/components/embeds/TwitterEmbed.tsx | 58 +
app/components/embeds/YouTubeEmbed.tsx | 32 +
app/components/embeds/index.ts | 4 +
app/components/layouts/ArticleLayout.tsx | 105 +
app/components/layouts/ArticleListLayout.tsx | 66 +
app/components/layouts/BaseLayout.tsx | 24 +
app/components/layouts/BookLayout.tsx | 126 +
app/components/layouts/BookListLayout.tsx | 93 +
app/components/layouts/ProjectLayout.tsx | 84 +
app/components/layouts/ProjectListLayout.tsx | 75 +
app/components/layouts/RootLayout.tsx | 79 +
app/components/mdx/index.tsx | 133 +
app/data/socials.ts | 25 +
app/entry.client.tsx | 7 +
app/entry.server.tsx | 129 +
app/hooks/useFocusManagement.ts | 65 +
app/hooks/useKeyboardNavigation.ts | 71 +
app/root.tsx | 118 +
app/routes/_index.tsx | 35 +
app/routes/api.embed-consent.ts | 27 +
app/routes/articles.$slug.tsx | 33 +
app/routes/articles._index.tsx | 14 +
app/routes/books.$slug.tsx | 39 +
app/routes/books._index.tsx | 14 +
app/routes/feed[.]xml.ts | 41 +
app/routes/projects.$slug.tsx | 34 +
app/routes/projects._index.tsx | 14 +
app/routes/sitemap[.]xml.ts | 74 +
app/styles/tailwind.css | 13 +
app/utils/content-relations.ts | 92 +
app/utils/content.ts | 38 +
app/utils/embed-consent.ts | 47 +
app/utils/navigation.ts | 19 +
app/utils/structured-data.ts | 85 +
app/utils/theme.server.ts | 30 +
.../astro.config.mjs | 0
.../package-lock.json | 0
.../src}/components/BackButton.astro | 0
.../src}/components/ExternalLink.astro | 0
.../src}/components/Footer.astro | 0
.../src}/components/Header.astro | 0
.../src}/components/ListHeader.astro | 0
.../src}/components/Particles.astro | 0
.../src}/components/ReadMoreButton.astro | 0
.../src}/components/SkipToContent.astro | 0
.../src}/components/SocialIcon.astro | 0
.../src}/components/SocialLinks.astro | 0
.../src}/components/TableOfContents.astro | 0
.../components/TableOfContentsHeading.astro | 0
.../embeds/AndererseitsInstagram.astro | 0
.../src}/components/icons/ArrowLeft.astro | 0
.../src}/components/icons/ArrowRight.astro | 0
.../src}/components/particles.json | 0
.../content/articles/2023-year-in-review.mdx | 0
.../der-wanderer-ueber-dem-nebelmeer.jpg | Bin
.../src}/content/books/a-liberated-mind.md | 0
.../src}/content/books/antifragile.md | 0
.../src}/content/books/black-swan.md | 0
.../src}/content/books/brave-new-world.md | 0
.../books/breaking-out-of-homeostasis.md | 0
.../src}/content/books/capitalist-realism.md | 0
.../books/covers/a-liberated-mind-cover.jpg | Bin
.../books/covers/antifragile-cover.jpg | Bin
.../books/covers/brave-new-world-cover.jpeg | Bin
.../breaking-out-of-homeostasis-cover.jpg | Bin
.../books/covers/capitalist-realism-cover.jpg | Bin
.../books/covers/dark-money-cover.jpeg | Bin
.../debt-the-first-5000-years-cover.jpg | Bin
.../src}/content/books/covers/flow-cover.jpg | Bin
.../covers/fooled-by-randomness-cover.jpg | Bin
.../content/books/covers/gateless-cover.jpg | Bin
...ote-this-book-because-i-love-you-cover.jpg | Bin
...die-ideologie-des-werbekoerpers-cover.webp | Bin
.../books/covers/kill-all-normies-cover.webp | Bin
.../books/covers/less-is-more-cover.webp | Bin
.../covers/mans-search-for-meaning-cover.jpg | Bin
.../books/covers/of-mice-and-men-cover.jpg | Bin
.../books/covers/seeing-that-frees-cover.jpg | Bin
.../books/covers/skin-in-the-game-cover.jpg | Bin
.../covers/survival-of-the-richest-cover.jpg | Bin
.../covers/the-art-of-learning-cover.jpg | Bin
.../books/covers/the-black-swan-cover.webp | Bin
.../the-count-of-monte-cristo-cover.jpg | Bin
.../content/books/covers/the-divide-cover.jpg | Bin
.../covers/the-mind-illuminated-cover.jpg | Bin
.../covers/the-paleo-manifesto-cover.jpg | Bin
.../the-science-of-enlightenment-cover.jpg | Bin
.../books/covers/vagabonding-cover.jpg | Bin
.../books/covers/we-learn-nothing-cover.jpg | Bin
.../books/covers/winners-take-all-cover.jpg | Bin
.../src}/content/books/dark-money.md | 0
.../books/debt-the-first-5000-years.md | 0
{src => astro-site/src}/content/books/flow.md | 0
.../content/books/fooled-by-randomness.md | 0
.../src}/content/books/gateless.md | 0
.../i-wrote-this-book-because-i-love-you.md | 0
...luencer-die-ideologie-des-werbekoerpers.md | 0
.../src}/content/books/kill-all-normies.md | 0
.../src}/content/books/less-is-more.md | 0
.../content/books/mans-search-for-meaning.md | 0
.../src}/content/books/of-mice-and-men.md | 0
.../src}/content/books/seeing-that-frees.md | 0
.../src}/content/books/skin-in-the-game.md | 0
.../content/books/survival-of-the-richest.md | 0
.../src}/content/books/the-art-of-learning.md | 0
.../books/the-count-of-monte-cristo.md | 0
.../src}/content/books/the-divide.md | 0
.../content/books/the-mind-illuminated.md | 0
.../src}/content/books/the-paleo-manifesto.md | 0
.../books/the-science-of-enlightenment.md | 0
.../src}/content/books/vagabonding.md | 0
.../src}/content/books/we-learn-nothing.md | 0
.../src}/content/books/winner-takes-all.md | 0
{src => astro-site/src}/content/config.ts | 0
.../src}/content/films/palm-springs.md | 0
.../src}/content/musicians/ski-aggu.md | 0
.../src}/content/pages/colophon.mdx | 0
.../src}/content/pages/imprint.md | 0
.../src}/content/pages/privacy-policy.md | 0
{src => astro-site/src}/content/pages/uses.md | 0
.../src}/content/projects/hasanhub-com.mdx | 0
.../src}/content/projects/hausgemacht.mdx | 0
.../hasanhub-plausible-april-may-2022.png | Bin
.../images/hasanhub-plausible-august-2022.png | Bin
.../hasanhub-plausible-december-2022.png | Bin
.../projects/images/hasanhub-screenshot.png | Bin
.../images/hausgemacht-screenshot.png | Bin
.../hausgemacht-vibechecker-screenshots.png | Bin
.../images/mitentscheiden-kabine-clip.gif | Bin
.../mitentscheiden-kabine-screenshot.png | Bin
.../images/mitentscheiden-results.png | Bin
.../images/mitentscheiden-screenshot.png | Bin
.../images/mitentscheiden-screenshots.jpg | Bin
.../images/mitentscheiden-shareables.jpg | Bin
.../preismonitor-plausible-may-july.png | Bin
.../images/preismonitor-screenshot.png | Bin
.../content/projects/mitentscheiden-at.mdx | 0
.../src}/content/projects/preismonitor-at.mdx | 0
.../src}/content/quotes/write-in-blood.md | 0
.../src}/content/shows/bojack-horseman.md | 0
{src => astro-site/src}/data/socials.ts | 0
{src => astro-site/src}/env.d.ts | 0
.../src}/images/chrcit-favicon.png | Bin
{src => astro-site/src}/images/cut-out.png | Bin
.../src}/images/home-profile-camera.jpg | Bin
.../src}/images/home-profile.jpg | Bin
.../src}/images/profile-frontal.jpg | Bin
.../src}/layouts/ArticleLayout.astro | 0
.../src}/layouts/BaseLayout.astro | 0
.../src}/layouts/ListLayout.astro | 0
.../src}/layouts/RootLayout.astro | 0
{src => astro-site/src}/pages/[slug].astro | 0
{src => astro-site/src}/pages/articles.astro | 0
.../src}/pages/articles/[slug].astro | 0
{src => astro-site/src}/pages/books.astro | 0
.../src}/pages/books/[slug].astro | 0
{src => astro-site/src}/pages/index.astro | 0
{src => astro-site/src}/pages/projects.astro | 0
.../src}/pages/projects/[slug].astro | 0
.../tailwind.config.mjs | 0
content/articles/2023-year-in-review.mdx | 283 +
content/articles/hello-world.mdx | 10 +
.../der-wanderer-ueber-dem-nebelmeer.jpg | Bin 0 -> 434154 bytes
content/books/a-liberated-mind.md | 11 +
content/books/antifragile.md | 11 +
content/books/black-swan.md | 11 +
content/books/brave-new-world.md | 11 +
content/books/breaking-out-of-homeostasis.md | 11 +
content/books/capitalist-realism.md | 11 +
.../books/covers/a-liberated-mind-cover.jpg | Bin 0 -> 666224 bytes
content/books/covers/antifragile-cover.jpg | Bin 0 -> 131148 bytes
.../books/covers/brave-new-world-cover.jpeg | Bin 0 -> 352599 bytes
.../breaking-out-of-homeostasis-cover.jpg | Bin 0 -> 43721 bytes
.../books/covers/capitalist-realism-cover.jpg | Bin 0 -> 203510 bytes
content/books/covers/dark-money-cover.jpeg | Bin 0 -> 278463 bytes
.../debt-the-first-5000-years-cover.jpg | Bin 0 -> 355209 bytes
content/books/covers/flow-cover.jpg | Bin 0 -> 209713 bytes
.../covers/fooled-by-randomness-cover.jpg | Bin 0 -> 128481 bytes
content/books/covers/gateless-cover.jpg | Bin 0 -> 35598 bytes
...ote-this-book-because-i-love-you-cover.jpg | Bin 0 -> 253315 bytes
...die-ideologie-des-werbekoerpers-cover.webp | Bin 0 -> 79410 bytes
.../books/covers/kill-all-normies-cover.webp | Bin 0 -> 115598 bytes
content/books/covers/less-is-more-cover.webp | Bin 0 -> 42316 bytes
.../covers/mans-search-for-meaning-cover.jpg | Bin 0 -> 139758 bytes
.../books/covers/of-mice-and-men-cover.jpg | Bin 0 -> 149443 bytes
.../books/covers/seeing-that-frees-cover.jpg | Bin 0 -> 95761 bytes
.../books/covers/skin-in-the-game-cover.jpg | Bin 0 -> 515708 bytes
.../covers/survival-of-the-richest-cover.jpg | Bin 0 -> 1373886 bytes
.../covers/the-art-of-learning-cover.jpg | Bin 0 -> 98123 bytes
.../books/covers/the-black-swan-cover.webp | Bin 0 -> 51328 bytes
.../the-count-of-monte-cristo-cover.jpg | Bin 0 -> 190004 bytes
content/books/covers/the-divide-cover.jpg | Bin 0 -> 35713 bytes
.../covers/the-mind-illuminated-cover.jpg | Bin 0 -> 77551 bytes
.../covers/the-paleo-manifesto-cover.jpg | Bin 0 -> 160081 bytes
.../the-science-of-enlightenment-cover.jpg | Bin 0 -> 148918 bytes
content/books/covers/vagabonding-cover.jpg | Bin 0 -> 96479 bytes
.../books/covers/we-learn-nothing-cover.jpg | Bin 0 -> 46499 bytes
.../books/covers/winners-take-all-cover.jpg | Bin 0 -> 74554 bytes
content/books/dark-money.md | 11 +
content/books/debt-the-first-5000-years.md | 11 +
content/books/flow.md | 11 +
content/books/fooled-by-randomness.md | 13 +
content/books/gateless.md | 11 +
.../i-wrote-this-book-because-i-love-you.md | 11 +
...luencer-die-ideologie-des-werbekoerpers.md | 11 +
content/books/kill-all-normies.md | 11 +
content/books/less-is-more.md | 11 +
content/books/mans-search-for-meaning.md | 11 +
content/books/of-mice-and-men.md | 13 +
content/books/seeing-that-frees.md | 11 +
content/books/skin-in-the-game.md | 11 +
content/books/survival-of-the-richest.md | 11 +
content/books/the-art-of-learning.md | 11 +
content/books/the-count-of-monte-cristo.md | 11 +
content/books/the-divide.md | 11 +
content/books/the-mind-illuminated.md | 11 +
content/books/the-paleo-manifesto.md | 11 +
content/books/the-science-of-enlightenment.md | 13 +
content/books/vagabonding.md | 11 +
content/books/we-learn-nothing.md | 11 +
content/books/winner-takes-all.md | 11 +
content/config.ts | 100 +
content/films/palm-springs.md | 5 +
content/musicians/ski-aggu.md | 4 +
content/pages/colophon.mdx | 26 +
content/pages/imprint.md | 10 +
content/pages/privacy-policy.md | 104 +
content/pages/uses.md | 73 +
content/projects/hasanhub-com.mdx | 112 +
content/projects/hausgemacht.mdx | 80 +
.../hasanhub-plausible-april-may-2022.png | Bin 0 -> 166567 bytes
.../images/hasanhub-plausible-august-2022.png | Bin 0 -> 160545 bytes
.../hasanhub-plausible-december-2022.png | Bin 0 -> 196052 bytes
.../projects/images/hasanhub-screenshot.png | Bin 0 -> 1702434 bytes
.../images/hausgemacht-screenshot.png | Bin 0 -> 379044 bytes
.../hausgemacht-vibechecker-screenshots.png | Bin 0 -> 322183 bytes
.../images/mitentscheiden-kabine-clip.gif | Bin 0 -> 960476 bytes
.../mitentscheiden-kabine-screenshot.png | Bin 0 -> 276592 bytes
.../images/mitentscheiden-results.png | Bin 0 -> 108296 bytes
.../images/mitentscheiden-screenshot.png | Bin 0 -> 255100 bytes
.../images/mitentscheiden-screenshots.jpg | Bin 0 -> 280141 bytes
.../images/mitentscheiden-shareables.jpg | Bin 0 -> 250214 bytes
.../preismonitor-plausible-may-july.png | Bin 0 -> 161346 bytes
.../images/preismonitor-screenshot.png | Bin 0 -> 260784 bytes
content/projects/mitentscheiden-at.mdx | 94 +
content/projects/preismonitor-at.mdx | 68 +
content/quotes/write-in-blood.md | 4 +
content/shows/bojack-horseman.md | 5 +
contentlayer.config.ts | 70 +
package.json | 47 +-
pnpm-lock.yaml | 9966 +++++++++++++++++
postcss.config.cjs | 6 +
tailwind.config.ts | 99 +
tsconfig.json | 30 +-
vite.config.ts | 24 +
267 files changed, 14234 insertions(+), 17 deletions(-)
create mode 100644 app/components/ErrorBoundary.tsx
create mode 100644 app/components/Footer.tsx
create mode 100644 app/components/Header.tsx
create mode 100644 app/components/Particles.tsx
create mode 100644 app/components/RelatedContent.tsx
create mode 100644 app/components/SocialIcon.tsx
create mode 100644 app/components/SocialLinks.tsx
create mode 100644 app/components/Toast.tsx
create mode 100644 app/components/embeds/EmbedOverlay.tsx
create mode 100644 app/components/embeds/InstagramEmbed.tsx
create mode 100644 app/components/embeds/RedditEmbed.tsx
create mode 100644 app/components/embeds/TwitterEmbed.tsx
create mode 100644 app/components/embeds/YouTubeEmbed.tsx
create mode 100644 app/components/embeds/index.ts
create mode 100644 app/components/layouts/ArticleLayout.tsx
create mode 100644 app/components/layouts/ArticleListLayout.tsx
create mode 100644 app/components/layouts/BaseLayout.tsx
create mode 100644 app/components/layouts/BookLayout.tsx
create mode 100644 app/components/layouts/BookListLayout.tsx
create mode 100644 app/components/layouts/ProjectLayout.tsx
create mode 100644 app/components/layouts/ProjectListLayout.tsx
create mode 100644 app/components/layouts/RootLayout.tsx
create mode 100644 app/components/mdx/index.tsx
create mode 100644 app/data/socials.ts
create mode 100644 app/entry.client.tsx
create mode 100644 app/entry.server.tsx
create mode 100644 app/hooks/useFocusManagement.ts
create mode 100644 app/hooks/useKeyboardNavigation.ts
create mode 100644 app/root.tsx
create mode 100644 app/routes/_index.tsx
create mode 100644 app/routes/api.embed-consent.ts
create mode 100644 app/routes/articles.$slug.tsx
create mode 100644 app/routes/articles._index.tsx
create mode 100644 app/routes/books.$slug.tsx
create mode 100644 app/routes/books._index.tsx
create mode 100644 app/routes/feed[.]xml.ts
create mode 100644 app/routes/projects.$slug.tsx
create mode 100644 app/routes/projects._index.tsx
create mode 100644 app/routes/sitemap[.]xml.ts
create mode 100644 app/styles/tailwind.css
create mode 100644 app/utils/content-relations.ts
create mode 100644 app/utils/content.ts
create mode 100644 app/utils/embed-consent.ts
create mode 100644 app/utils/navigation.ts
create mode 100644 app/utils/structured-data.ts
create mode 100644 app/utils/theme.server.ts
rename astro.config.mjs => astro-site/astro.config.mjs (100%)
rename package-lock.json => astro-site/package-lock.json (100%)
rename {src => astro-site/src}/components/BackButton.astro (100%)
rename {src => astro-site/src}/components/ExternalLink.astro (100%)
rename {src => astro-site/src}/components/Footer.astro (100%)
rename {src => astro-site/src}/components/Header.astro (100%)
rename {src => astro-site/src}/components/ListHeader.astro (100%)
rename {src => astro-site/src}/components/Particles.astro (100%)
rename {src => astro-site/src}/components/ReadMoreButton.astro (100%)
rename {src => astro-site/src}/components/SkipToContent.astro (100%)
rename {src => astro-site/src}/components/SocialIcon.astro (100%)
rename {src => astro-site/src}/components/SocialLinks.astro (100%)
rename {src => astro-site/src}/components/TableOfContents.astro (100%)
rename {src => astro-site/src}/components/TableOfContentsHeading.astro (100%)
rename {src => astro-site/src}/components/embeds/AndererseitsInstagram.astro (100%)
rename {src => astro-site/src}/components/icons/ArrowLeft.astro (100%)
rename {src => astro-site/src}/components/icons/ArrowRight.astro (100%)
rename {src => astro-site/src}/components/particles.json (100%)
rename {src => astro-site/src}/content/articles/2023-year-in-review.mdx (100%)
rename {src => astro-site/src}/content/articles/images/der-wanderer-ueber-dem-nebelmeer.jpg (100%)
rename {src => astro-site/src}/content/books/a-liberated-mind.md (100%)
rename {src => astro-site/src}/content/books/antifragile.md (100%)
rename {src => astro-site/src}/content/books/black-swan.md (100%)
rename {src => astro-site/src}/content/books/brave-new-world.md (100%)
rename {src => astro-site/src}/content/books/breaking-out-of-homeostasis.md (100%)
rename {src => astro-site/src}/content/books/capitalist-realism.md (100%)
rename {src => astro-site/src}/content/books/covers/a-liberated-mind-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/antifragile-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/brave-new-world-cover.jpeg (100%)
rename {src => astro-site/src}/content/books/covers/breaking-out-of-homeostasis-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/capitalist-realism-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/dark-money-cover.jpeg (100%)
rename {src => astro-site/src}/content/books/covers/debt-the-first-5000-years-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/flow-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/fooled-by-randomness-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/gateless-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/i-wrote-this-book-because-i-love-you-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/influencer-die-ideologie-des-werbekoerpers-cover.webp (100%)
rename {src => astro-site/src}/content/books/covers/kill-all-normies-cover.webp (100%)
rename {src => astro-site/src}/content/books/covers/less-is-more-cover.webp (100%)
rename {src => astro-site/src}/content/books/covers/mans-search-for-meaning-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/of-mice-and-men-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/seeing-that-frees-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/skin-in-the-game-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/survival-of-the-richest-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/the-art-of-learning-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/the-black-swan-cover.webp (100%)
rename {src => astro-site/src}/content/books/covers/the-count-of-monte-cristo-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/the-divide-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/the-mind-illuminated-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/the-paleo-manifesto-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/the-science-of-enlightenment-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/vagabonding-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/we-learn-nothing-cover.jpg (100%)
rename {src => astro-site/src}/content/books/covers/winners-take-all-cover.jpg (100%)
rename {src => astro-site/src}/content/books/dark-money.md (100%)
rename {src => astro-site/src}/content/books/debt-the-first-5000-years.md (100%)
rename {src => astro-site/src}/content/books/flow.md (100%)
rename {src => astro-site/src}/content/books/fooled-by-randomness.md (100%)
rename {src => astro-site/src}/content/books/gateless.md (100%)
rename {src => astro-site/src}/content/books/i-wrote-this-book-because-i-love-you.md (100%)
rename {src => astro-site/src}/content/books/influencer-die-ideologie-des-werbekoerpers.md (100%)
rename {src => astro-site/src}/content/books/kill-all-normies.md (100%)
rename {src => astro-site/src}/content/books/less-is-more.md (100%)
rename {src => astro-site/src}/content/books/mans-search-for-meaning.md (100%)
rename {src => astro-site/src}/content/books/of-mice-and-men.md (100%)
rename {src => astro-site/src}/content/books/seeing-that-frees.md (100%)
rename {src => astro-site/src}/content/books/skin-in-the-game.md (100%)
rename {src => astro-site/src}/content/books/survival-of-the-richest.md (100%)
rename {src => astro-site/src}/content/books/the-art-of-learning.md (100%)
rename {src => astro-site/src}/content/books/the-count-of-monte-cristo.md (100%)
rename {src => astro-site/src}/content/books/the-divide.md (100%)
rename {src => astro-site/src}/content/books/the-mind-illuminated.md (100%)
rename {src => astro-site/src}/content/books/the-paleo-manifesto.md (100%)
rename {src => astro-site/src}/content/books/the-science-of-enlightenment.md (100%)
rename {src => astro-site/src}/content/books/vagabonding.md (100%)
rename {src => astro-site/src}/content/books/we-learn-nothing.md (100%)
rename {src => astro-site/src}/content/books/winner-takes-all.md (100%)
rename {src => astro-site/src}/content/config.ts (100%)
rename {src => astro-site/src}/content/films/palm-springs.md (100%)
rename {src => astro-site/src}/content/musicians/ski-aggu.md (100%)
rename {src => astro-site/src}/content/pages/colophon.mdx (100%)
rename {src => astro-site/src}/content/pages/imprint.md (100%)
rename {src => astro-site/src}/content/pages/privacy-policy.md (100%)
rename {src => astro-site/src}/content/pages/uses.md (100%)
rename {src => astro-site/src}/content/projects/hasanhub-com.mdx (100%)
rename {src => astro-site/src}/content/projects/hausgemacht.mdx (100%)
rename {src => astro-site/src}/content/projects/images/hasanhub-plausible-april-may-2022.png (100%)
rename {src => astro-site/src}/content/projects/images/hasanhub-plausible-august-2022.png (100%)
rename {src => astro-site/src}/content/projects/images/hasanhub-plausible-december-2022.png (100%)
rename {src => astro-site/src}/content/projects/images/hasanhub-screenshot.png (100%)
rename {src => astro-site/src}/content/projects/images/hausgemacht-screenshot.png (100%)
rename {src => astro-site/src}/content/projects/images/hausgemacht-vibechecker-screenshots.png (100%)
rename {src => astro-site/src}/content/projects/images/mitentscheiden-kabine-clip.gif (100%)
rename {src => astro-site/src}/content/projects/images/mitentscheiden-kabine-screenshot.png (100%)
rename {src => astro-site/src}/content/projects/images/mitentscheiden-results.png (100%)
rename {src => astro-site/src}/content/projects/images/mitentscheiden-screenshot.png (100%)
rename {src => astro-site/src}/content/projects/images/mitentscheiden-screenshots.jpg (100%)
rename {src => astro-site/src}/content/projects/images/mitentscheiden-shareables.jpg (100%)
rename {src => astro-site/src}/content/projects/images/preismonitor-plausible-may-july.png (100%)
rename {src => astro-site/src}/content/projects/images/preismonitor-screenshot.png (100%)
rename {src => astro-site/src}/content/projects/mitentscheiden-at.mdx (100%)
rename {src => astro-site/src}/content/projects/preismonitor-at.mdx (100%)
rename {src => astro-site/src}/content/quotes/write-in-blood.md (100%)
rename {src => astro-site/src}/content/shows/bojack-horseman.md (100%)
rename {src => astro-site/src}/data/socials.ts (100%)
rename {src => astro-site/src}/env.d.ts (100%)
rename {src => astro-site/src}/images/chrcit-favicon.png (100%)
rename {src => astro-site/src}/images/cut-out.png (100%)
rename {src => astro-site/src}/images/home-profile-camera.jpg (100%)
rename {src => astro-site/src}/images/home-profile.jpg (100%)
rename {src => astro-site/src}/images/profile-frontal.jpg (100%)
rename {src => astro-site/src}/layouts/ArticleLayout.astro (100%)
rename {src => astro-site/src}/layouts/BaseLayout.astro (100%)
rename {src => astro-site/src}/layouts/ListLayout.astro (100%)
rename {src => astro-site/src}/layouts/RootLayout.astro (100%)
rename {src => astro-site/src}/pages/[slug].astro (100%)
rename {src => astro-site/src}/pages/articles.astro (100%)
rename {src => astro-site/src}/pages/articles/[slug].astro (100%)
rename {src => astro-site/src}/pages/books.astro (100%)
rename {src => astro-site/src}/pages/books/[slug].astro (100%)
rename {src => astro-site/src}/pages/index.astro (100%)
rename {src => astro-site/src}/pages/projects.astro (100%)
rename {src => astro-site/src}/pages/projects/[slug].astro (100%)
rename tailwind.config.mjs => astro-site/tailwind.config.mjs (100%)
create mode 100644 content/articles/2023-year-in-review.mdx
create mode 100644 content/articles/hello-world.mdx
create mode 100644 content/articles/images/der-wanderer-ueber-dem-nebelmeer.jpg
create mode 100644 content/books/a-liberated-mind.md
create mode 100644 content/books/antifragile.md
create mode 100644 content/books/black-swan.md
create mode 100644 content/books/brave-new-world.md
create mode 100644 content/books/breaking-out-of-homeostasis.md
create mode 100644 content/books/capitalist-realism.md
create mode 100644 content/books/covers/a-liberated-mind-cover.jpg
create mode 100644 content/books/covers/antifragile-cover.jpg
create mode 100644 content/books/covers/brave-new-world-cover.jpeg
create mode 100644 content/books/covers/breaking-out-of-homeostasis-cover.jpg
create mode 100644 content/books/covers/capitalist-realism-cover.jpg
create mode 100644 content/books/covers/dark-money-cover.jpeg
create mode 100644 content/books/covers/debt-the-first-5000-years-cover.jpg
create mode 100644 content/books/covers/flow-cover.jpg
create mode 100644 content/books/covers/fooled-by-randomness-cover.jpg
create mode 100644 content/books/covers/gateless-cover.jpg
create mode 100644 content/books/covers/i-wrote-this-book-because-i-love-you-cover.jpg
create mode 100644 content/books/covers/influencer-die-ideologie-des-werbekoerpers-cover.webp
create mode 100644 content/books/covers/kill-all-normies-cover.webp
create mode 100644 content/books/covers/less-is-more-cover.webp
create mode 100644 content/books/covers/mans-search-for-meaning-cover.jpg
create mode 100644 content/books/covers/of-mice-and-men-cover.jpg
create mode 100644 content/books/covers/seeing-that-frees-cover.jpg
create mode 100644 content/books/covers/skin-in-the-game-cover.jpg
create mode 100644 content/books/covers/survival-of-the-richest-cover.jpg
create mode 100644 content/books/covers/the-art-of-learning-cover.jpg
create mode 100644 content/books/covers/the-black-swan-cover.webp
create mode 100644 content/books/covers/the-count-of-monte-cristo-cover.jpg
create mode 100644 content/books/covers/the-divide-cover.jpg
create mode 100644 content/books/covers/the-mind-illuminated-cover.jpg
create mode 100644 content/books/covers/the-paleo-manifesto-cover.jpg
create mode 100644 content/books/covers/the-science-of-enlightenment-cover.jpg
create mode 100644 content/books/covers/vagabonding-cover.jpg
create mode 100644 content/books/covers/we-learn-nothing-cover.jpg
create mode 100644 content/books/covers/winners-take-all-cover.jpg
create mode 100644 content/books/dark-money.md
create mode 100644 content/books/debt-the-first-5000-years.md
create mode 100644 content/books/flow.md
create mode 100644 content/books/fooled-by-randomness.md
create mode 100644 content/books/gateless.md
create mode 100644 content/books/i-wrote-this-book-because-i-love-you.md
create mode 100644 content/books/influencer-die-ideologie-des-werbekoerpers.md
create mode 100644 content/books/kill-all-normies.md
create mode 100644 content/books/less-is-more.md
create mode 100644 content/books/mans-search-for-meaning.md
create mode 100644 content/books/of-mice-and-men.md
create mode 100644 content/books/seeing-that-frees.md
create mode 100644 content/books/skin-in-the-game.md
create mode 100644 content/books/survival-of-the-richest.md
create mode 100644 content/books/the-art-of-learning.md
create mode 100644 content/books/the-count-of-monte-cristo.md
create mode 100644 content/books/the-divide.md
create mode 100644 content/books/the-mind-illuminated.md
create mode 100644 content/books/the-paleo-manifesto.md
create mode 100644 content/books/the-science-of-enlightenment.md
create mode 100644 content/books/vagabonding.md
create mode 100644 content/books/we-learn-nothing.md
create mode 100644 content/books/winner-takes-all.md
create mode 100644 content/config.ts
create mode 100644 content/films/palm-springs.md
create mode 100644 content/musicians/ski-aggu.md
create mode 100644 content/pages/colophon.mdx
create mode 100644 content/pages/imprint.md
create mode 100644 content/pages/privacy-policy.md
create mode 100644 content/pages/uses.md
create mode 100644 content/projects/hasanhub-com.mdx
create mode 100644 content/projects/hausgemacht.mdx
create mode 100644 content/projects/images/hasanhub-plausible-april-may-2022.png
create mode 100644 content/projects/images/hasanhub-plausible-august-2022.png
create mode 100644 content/projects/images/hasanhub-plausible-december-2022.png
create mode 100644 content/projects/images/hasanhub-screenshot.png
create mode 100644 content/projects/images/hausgemacht-screenshot.png
create mode 100644 content/projects/images/hausgemacht-vibechecker-screenshots.png
create mode 100644 content/projects/images/mitentscheiden-kabine-clip.gif
create mode 100644 content/projects/images/mitentscheiden-kabine-screenshot.png
create mode 100644 content/projects/images/mitentscheiden-results.png
create mode 100644 content/projects/images/mitentscheiden-screenshot.png
create mode 100644 content/projects/images/mitentscheiden-screenshots.jpg
create mode 100644 content/projects/images/mitentscheiden-shareables.jpg
create mode 100644 content/projects/images/preismonitor-plausible-may-july.png
create mode 100644 content/projects/images/preismonitor-screenshot.png
create mode 100644 content/projects/mitentscheiden-at.mdx
create mode 100644 content/projects/preismonitor-at.mdx
create mode 100644 content/quotes/write-in-blood.md
create mode 100644 content/shows/bojack-horseman.md
create mode 100644 contentlayer.config.ts
create mode 100644 pnpm-lock.yaml
create mode 100644 postcss.config.cjs
create mode 100644 tailwind.config.ts
create mode 100644 vite.config.ts
diff --git a/.gitignore b/.gitignore
index 6d4c0aa..50c7030 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,5 @@ pnpm-debug.log*
# macOS-specific files
.DS_Store
+
+.contentlayer/
\ No newline at end of file
diff --git a/app/components/ErrorBoundary.tsx b/app/components/ErrorBoundary.tsx
new file mode 100644
index 0000000..17a921c
--- /dev/null
+++ b/app/components/ErrorBoundary.tsx
@@ -0,0 +1,111 @@
+import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
+import { RootLayout } from "./layouts/RootLayout";
+
+export function ErrorBoundary() {
+ const error = useRouteError();
+
+ if (isRouteErrorResponse(error)) {
+ switch (error.status) {
+ case 404:
+ return (
+
+
+
+
Page Not Found
+
+ The page you're looking for doesn't exist. Please check the
+ URL and try again.
+
+
+
+ ← Go back home
+
+
+
+
+
+ );
+ case 401:
+ return (
+
+
+
+
Unauthorized
+
+ You don't have permission to access this page. Please log in
+ and try again.
+
+
+
+ ← Go back home
+
+
+
+
+
+ );
+ default:
+ return (
+
+
+
+
Error
+
+ Something went wrong. Please try again later. If the problem
+ persists, please contact support.
+
+
+
+ ← Go back home
+
+
+ {process.env.NODE_ENV === "development" && (
+
+ {error.data.message || JSON.stringify(error.data, null, 2)}
+
+ )}
+
+
+
+ );
+ }
+ }
+
+ return (
+
+
+
+
Error
+
+ Something went wrong. Please try again later. If the problem
+ persists, please contact support.
+
+
+
+ ← Go back home
+
+
+ {process.env.NODE_ENV === "development" && (
+
+ {error instanceof Error
+ ? error.message
+ : "Unknown error occurred"}
+
+ )}
+
+
+
+ );
+}
diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx
new file mode 100644
index 0000000..3523b98
--- /dev/null
+++ b/app/components/Footer.tsx
@@ -0,0 +1,32 @@
+import { Link } from "@remix-run/react";
+import { SocialLinks } from "./SocialLinks";
+
+const footerLinks = [
+ { name: "Colophon", href: "/colophon" },
+ { name: "Privacy Policy", href: "/privacy-policy" },
+ { name: "Imprint", href: "/imprint" },
+] as const;
+
+export function Footer() {
+ return (
+
+
+
+
+
+ {footerLinks.map((link) => (
+
+
+ {link.name}
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/app/components/Header.tsx b/app/components/Header.tsx
new file mode 100644
index 0000000..1cd6d10
--- /dev/null
+++ b/app/components/Header.tsx
@@ -0,0 +1,74 @@
+import { Link, useLocation } from "@remix-run/react";
+import { type ActiveTabType, navigationLinks } from "~/utils/navigation";
+import { SocialLinks } from "./SocialLinks";
+import clsx from "clsx";
+
+interface HeaderProps {
+ activeTab?: ActiveTabType;
+}
+
+export function Header({ activeTab }: HeaderProps) {
+ const location = useLocation();
+
+ return (
+
+
+
+
+
+
+
+
+ {navigationLinks.map((link) => {
+ const isActive = activeTab
+ ? activeTab === link.href
+ : location.pathname === link.href;
+
+ return (
+
+
+
+ /
+
+
+ {link.name}
+
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/app/components/Particles.tsx b/app/components/Particles.tsx
new file mode 100644
index 0000000..5966cc1
--- /dev/null
+++ b/app/components/Particles.tsx
@@ -0,0 +1,85 @@
+import { useCallback } from "react";
+import type { Container, Engine } from "tsparticles-engine";
+import { loadFull } from "tsparticles";
+import Particles from "react-tsparticles";
+
+export function Particles({ className }: { className?: string }) {
+ const particlesInit = useCallback(async (engine: Engine) => {
+ await loadFull(engine);
+ }, []);
+
+ const particlesLoaded = useCallback(
+ async (container: Container | undefined) => {
+ await container?.refresh();
+ },
+ [],
+ );
+
+ return (
+
+ );
+}
diff --git a/app/components/RelatedContent.tsx b/app/components/RelatedContent.tsx
new file mode 100644
index 0000000..118e993
--- /dev/null
+++ b/app/components/RelatedContent.tsx
@@ -0,0 +1,55 @@
+import { Link } from "@remix-run/react";
+
+interface RelatedContentItem {
+ title: string;
+ description: string;
+ slug: string;
+ type: "article" | "book" | "project";
+ tags: string[];
+}
+
+interface RelatedContentProps {
+ items: RelatedContentItem[];
+ title?: string;
+}
+
+export function RelatedContent({
+ items,
+ title = "Related Content",
+}: RelatedContentProps) {
+ if (items.length === 0) return null;
+
+ return (
+
+ );
+}
diff --git a/app/components/SocialIcon.tsx b/app/components/SocialIcon.tsx
new file mode 100644
index 0000000..3fc50c9
--- /dev/null
+++ b/app/components/SocialIcon.tsx
@@ -0,0 +1,69 @@
+import { type SocialType } from "~/data/socials";
+import clsx from "clsx";
+
+interface SocialIconProps {
+ type: SocialType;
+ size: number;
+ className?: string;
+}
+
+export function SocialIcon({ type, size, className }: SocialIconProps) {
+ const iconProps = {
+ className: clsx("lucide", `lucide-${type}`, className),
+ xmlns: "http://www.w3.org/2000/svg",
+ width: size,
+ height: size,
+ viewBox: "0 0 24 24",
+ fill: "none",
+ stroke: "currentColor",
+ strokeWidth: "2",
+ strokeLinecap: "round",
+ strokeLinejoin: "round",
+ };
+
+ switch (type) {
+ case "github":
+ return (
+
+
+
+
+ );
+
+ case "twitter":
+ return (
+
+
+
+ );
+
+ case "linkedin":
+ return (
+
+
+
+
+
+ );
+
+ case "youtube":
+ return (
+
+
+
+
+ );
+
+ case "instagram":
+ return (
+
+
+
+
+
+ );
+
+ default:
+ return null;
+ }
+}
diff --git a/app/components/SocialLinks.tsx b/app/components/SocialLinks.tsx
new file mode 100644
index 0000000..d177570
--- /dev/null
+++ b/app/components/SocialLinks.tsx
@@ -0,0 +1,33 @@
+import { socialLinks } from "~/data/socials";
+import { SocialIcon } from "./SocialIcon";
+import clsx from "clsx";
+
+interface SocialLinksProps {
+ size?: number;
+ className?: string;
+}
+
+export function SocialLinks({ size = 16, className }: SocialLinksProps) {
+ return (
+
+ {socialLinks.map((link) => (
+
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/app/components/Toast.tsx b/app/components/Toast.tsx
new file mode 100644
index 0000000..a6a4906
--- /dev/null
+++ b/app/components/Toast.tsx
@@ -0,0 +1,113 @@
+import { useEffect, useState } from "react";
+
+interface ToastProps {
+ message: string;
+ type?: "success" | "error" | "info";
+ duration?: number;
+ onClose?: () => void;
+}
+
+export function Toast({
+ message,
+ type = "info",
+ duration = 3000,
+ onClose,
+}: ToastProps) {
+ const [isVisible, setIsVisible] = useState(true);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setIsVisible(false);
+ onClose?.();
+ }, duration);
+
+ return () => clearTimeout(timer);
+ }, [duration, onClose]);
+
+ if (!isVisible) return null;
+
+ const bgColor = {
+ success: "bg-green-500",
+ error: "bg-red-500",
+ info: "bg-blue-500",
+ }[type];
+
+ return (
+
+
+
{message}
+
{
+ setIsVisible(false);
+ onClose?.();
+ }}
+ className="ml-2 rounded-full p-1 hover:bg-white/10"
+ aria-label="Close notification"
+ >
+
+
+
+
+
+
+ );
+}
+
+interface ToastManagerProps {
+ children: React.ReactNode;
+}
+
+interface ToastState {
+ id: number;
+ message: string;
+ type?: "success" | "error" | "info";
+ duration?: number;
+}
+
+export function ToastManager({ children }: ToastManagerProps) {
+ const [toasts, setToasts] = useState([]);
+
+ const addToast = (
+ message: string,
+ type: "success" | "error" | "info" = "info",
+ duration = 3000,
+ ) => {
+ const id = Date.now();
+ setToasts((prev) => [...prev, { id, message, type, duration }]);
+ };
+
+ const removeToast = (id: number) => {
+ setToasts((prev) => prev.filter((toast) => toast.id !== id));
+ };
+
+ return (
+ <>
+ {children}
+
+ {toasts.map((toast) => (
+ removeToast(toast.id)}
+ />
+ ))}
+
+ >
+ );
+}
diff --git a/app/components/embeds/EmbedOverlay.tsx b/app/components/embeds/EmbedOverlay.tsx
new file mode 100644
index 0000000..39a90d6
--- /dev/null
+++ b/app/components/embeds/EmbedOverlay.tsx
@@ -0,0 +1,57 @@
+import { useFetcher } from "@remix-run/react";
+import { type EmbedType } from "~/utils/embed-consent";
+import clsx from "clsx";
+
+interface EmbedOverlayProps {
+ type: EmbedType;
+ title: string;
+ description: string;
+ hasConsent: boolean;
+ children: React.ReactNode;
+}
+
+export function EmbedOverlay({
+ type,
+ title,
+ description,
+ hasConsent,
+ children,
+}: EmbedOverlayProps) {
+ const fetcher = useFetcher();
+ const isLoading = fetcher.state !== "idle";
+
+ if (hasConsent) {
+ return <>{children}>;
+ }
+
+ return (
+
+
+
{title}
+
{description}
+
+
+
+
+
+
+ {isLoading ? "Loading..." : "Load Embed"}
+
+
+
+
+ By clicking "Load Embed", you consent to loading content from{" "}
+ {type} . This setting
+ will be remembered for future embeds.
+
+
+ );
+}
diff --git a/app/components/embeds/InstagramEmbed.tsx b/app/components/embeds/InstagramEmbed.tsx
new file mode 100644
index 0000000..5fc93f4
--- /dev/null
+++ b/app/components/embeds/InstagramEmbed.tsx
@@ -0,0 +1,83 @@
+import { useEffect, useRef } from "react";
+import { EmbedOverlay } from "./EmbedOverlay";
+
+interface InstagramEmbedProps {
+ postId: string;
+ hasConsent: boolean;
+}
+
+declare global {
+ interface Window {
+ instgrm?: {
+ Embeds: {
+ process: () => void;
+ };
+ };
+ }
+}
+
+export function InstagramEmbed({ postId, hasConsent }: InstagramEmbedProps) {
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ if (!hasConsent) return;
+
+ // Load Instagram embed script if not already loaded
+ if (!window.instgrm) {
+ const script = document.createElement("script");
+ script.src = "//www.instagram.com/embed.js";
+ script.async = true;
+ document.body.appendChild(script);
+ } else {
+ // If script is already loaded, process the embed
+ window.instgrm.Embeds.process();
+ }
+ }, [hasConsent]);
+
+ return (
+
+
+
+ );
+}
diff --git a/app/components/embeds/RedditEmbed.tsx b/app/components/embeds/RedditEmbed.tsx
new file mode 100644
index 0000000..15817d1
--- /dev/null
+++ b/app/components/embeds/RedditEmbed.tsx
@@ -0,0 +1,49 @@
+import { useEffect, useRef } from "react";
+import { EmbedOverlay } from "./EmbedOverlay";
+
+interface RedditEmbedProps {
+ postUrl: string;
+ hasConsent: boolean;
+}
+
+declare global {
+ interface Window {
+ rembeddit?: {
+ init: () => void;
+ };
+ }
+}
+
+export function RedditEmbed({ postUrl, hasConsent }: RedditEmbedProps) {
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ if (!hasConsent) return;
+
+ // Load Reddit embed script if not already loaded
+ if (!window.rembeddit) {
+ const script = document.createElement("script");
+ script.src = "https://embed.reddit.com/widgets.js";
+ script.async = true;
+ document.body.appendChild(script);
+ } else {
+ // If script is already loaded, initialize the embed
+ window.rembeddit.init();
+ }
+ }, [hasConsent]);
+
+ return (
+
+
+
+ );
+}
diff --git a/app/components/embeds/TwitterEmbed.tsx b/app/components/embeds/TwitterEmbed.tsx
new file mode 100644
index 0000000..240c421
--- /dev/null
+++ b/app/components/embeds/TwitterEmbed.tsx
@@ -0,0 +1,58 @@
+import { useEffect, useRef } from "react";
+import { EmbedOverlay } from "./EmbedOverlay";
+
+interface TwitterEmbedProps {
+ tweetId: string;
+ hasConsent: boolean;
+}
+
+declare global {
+ interface Window {
+ twttr?: {
+ widgets: {
+ load: (element?: HTMLElement) => void;
+ };
+ };
+ }
+}
+
+export function TwitterEmbed({ tweetId, hasConsent }: TwitterEmbedProps) {
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ if (!hasConsent) return;
+
+ // Load Twitter widget script if not already loaded
+ if (!window.twttr) {
+ const script = document.createElement("script");
+ script.src = "https://platform.twitter.com/widgets.js";
+ script.async = true;
+ document.body.appendChild(script);
+ } else {
+ // If script is already loaded, render the tweet
+ window.twttr.widgets.load(containerRef.current);
+ }
+ }, [hasConsent]);
+
+ return (
+
+
+
+ );
+}
diff --git a/app/components/embeds/YouTubeEmbed.tsx b/app/components/embeds/YouTubeEmbed.tsx
new file mode 100644
index 0000000..26d9838
--- /dev/null
+++ b/app/components/embeds/YouTubeEmbed.tsx
@@ -0,0 +1,32 @@
+import { EmbedOverlay } from "./EmbedOverlay";
+
+interface YouTubeEmbedProps {
+ videoId: string;
+ hasConsent: boolean;
+ title?: string;
+}
+
+export function YouTubeEmbed({
+ videoId,
+ hasConsent,
+ title,
+}: YouTubeEmbedProps) {
+ return (
+
+
+ VIDEO
+
+
+ );
+}
diff --git a/app/components/embeds/index.ts b/app/components/embeds/index.ts
new file mode 100644
index 0000000..55314e4
--- /dev/null
+++ b/app/components/embeds/index.ts
@@ -0,0 +1,4 @@
+export { YouTubeEmbed } from "./YouTubeEmbed";
+export { TwitterEmbed } from "./TwitterEmbed";
+export { InstagramEmbed } from "./InstagramEmbed";
+export { RedditEmbed } from "./RedditEmbed";
diff --git a/app/components/layouts/ArticleLayout.tsx b/app/components/layouts/ArticleLayout.tsx
new file mode 100644
index 0000000..904865f
--- /dev/null
+++ b/app/components/layouts/ArticleLayout.tsx
@@ -0,0 +1,105 @@
+import { Link } from "@remix-run/react";
+import { RootLayout } from "./RootLayout";
+import { type EmbedConsent } from "~/utils/embed-consent";
+import { getMDXComponents } from "../mdx";
+import { useMemo } from "react";
+import { RelatedContent } from "../RelatedContent";
+import { findRelatedContent } from "~/utils/content-relations";
+
+interface ArticleLayoutProps {
+ title: string;
+ description: string;
+ publishedAt: string;
+ updatedAt?: string;
+ author: string;
+ tags: string[];
+ slug: string;
+ content: string;
+ embedConsent: EmbedConsent;
+}
+
+export function ArticleLayout({
+ title,
+ description,
+ publishedAt,
+ updatedAt,
+ author,
+ tags,
+ slug,
+ content,
+ embedConsent,
+}: ArticleLayoutProps) {
+ const components = useMemo(
+ () => getMDXComponents({ embedConsent }),
+ [embedConsent],
+ );
+
+ const relatedContent = findRelatedContent("article", slug, tags);
+
+ return (
+
+
+
+
+ ← Back to articles
+
+
+
+
+
+ {title}
+ {description}
+
+
+ By {author}
+ •
+
+ {new Date(publishedAt).toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ })}
+
+ {updatedAt && (
+ <>
+ •
+
+ Updated{" "}
+ {new Date(updatedAt).toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ })}
+
+ >
+ )}
+
+ {tags.length > 0 && (
+
+ {tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+
+
+ {content}
+
+
+
+
+
+ );
+}
diff --git a/app/components/layouts/ArticleListLayout.tsx b/app/components/layouts/ArticleListLayout.tsx
new file mode 100644
index 0000000..17f0b40
--- /dev/null
+++ b/app/components/layouts/ArticleListLayout.tsx
@@ -0,0 +1,66 @@
+import { Link } from "@remix-run/react";
+import { RootLayout } from "./RootLayout";
+
+interface Article {
+ title: string;
+ description: string;
+ slug: string;
+ publishedAt?: string;
+}
+
+interface ArticleListLayoutProps {
+ articles: Article[];
+}
+
+export function ArticleListLayout({ articles }: ArticleListLayoutProps) {
+ return (
+
+
+
+
Articles
+
+ I write about software development, design, and other topics that
+ interest me.
+
+
+
+
+ {articles.map((article) => (
+
+
+
+
+ {article.title}
+
+ {article.publishedAt && (
+
+ {new Date(article.publishedAt).toLocaleDateString(
+ "en-US",
+ {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ },
+ )}
+
+ )}
+ {article.description}
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/components/layouts/BaseLayout.tsx b/app/components/layouts/BaseLayout.tsx
new file mode 100644
index 0000000..2a23b3f
--- /dev/null
+++ b/app/components/layouts/BaseLayout.tsx
@@ -0,0 +1,24 @@
+import { type PropsWithChildren } from "react";
+
+interface BaseLayoutProps extends PropsWithChildren {
+ title?: string;
+ description?: string;
+}
+
+export function BaseLayout({ children, title, description }: BaseLayoutProps) {
+ const pageTitle = title ? `${title} | chrcit.com` : "Hi, I'm Christian";
+ const pageDescription = description ?? "I create things for the internet";
+
+ return (
+
+ {/* Plausible Analytics */}
+
+ {children}
+
+ );
+}
diff --git a/app/components/layouts/BookLayout.tsx b/app/components/layouts/BookLayout.tsx
new file mode 100644
index 0000000..0159d8d
--- /dev/null
+++ b/app/components/layouts/BookLayout.tsx
@@ -0,0 +1,126 @@
+import { Link } from "@remix-run/react";
+import { RootLayout } from "./RootLayout";
+import { type EmbedConsent } from "~/utils/embed-consent";
+import { getMDXComponents } from "../mdx";
+import { useMemo } from "react";
+import { RelatedContent } from "../RelatedContent";
+import { findRelatedContent } from "~/utils/content-relations";
+
+interface BookLayoutProps {
+ title: string;
+ description: string;
+ author: string;
+ rating: number;
+ cover: string;
+ year: number;
+ category: string;
+ tags: string[];
+ url: string;
+ slug: string;
+ content: string;
+ embedConsent: EmbedConsent;
+}
+
+export function BookLayout({
+ title,
+ description,
+ author,
+ rating,
+ cover,
+ year,
+ category,
+ tags,
+ url,
+ slug,
+ content,
+ embedConsent,
+}: BookLayoutProps) {
+ const components = useMemo(
+ () => getMDXComponents({ embedConsent }),
+ [embedConsent],
+ );
+
+ const relatedContent = findRelatedContent("book", slug, tags);
+
+ return (
+
+
+
+
+ ← Back to books
+
+
+
+
+
+
+
+
+
+
+
{title}
+
by {author}
+
+
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ★
+
+ ))}
+
+
({rating}/5)
+
+
+
{description}
+
+
+
+ {category}
+
+
+ {year}
+
+ {tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+
+ View on Amazon
+
+
+
+
+ {content}
+
+
+
+
+
+ );
+}
diff --git a/app/components/layouts/BookListLayout.tsx b/app/components/layouts/BookListLayout.tsx
new file mode 100644
index 0000000..beca8d9
--- /dev/null
+++ b/app/components/layouts/BookListLayout.tsx
@@ -0,0 +1,93 @@
+import { Link } from "@remix-run/react";
+import { RootLayout } from "./RootLayout";
+
+interface Book {
+ title: string;
+ description: string;
+ slug: string;
+ author: string;
+ rating: number;
+ cover: string;
+ year: number;
+ category: string;
+ tags: string[];
+}
+
+interface BookListLayoutProps {
+ books: Book[];
+}
+
+export function BookListLayout({ books }: BookListLayoutProps) {
+ return (
+
+
+
+
Books
+
+ A collection of books I've read and recommend. I try to read a mix
+ of fiction and non-fiction, with a focus on technology, science, and
+ philosophy.
+
+
+
+
+ {books.map((book) => (
+
+
+
+
+
+
+
+ {book.title}
+
+
by {book.author}
+
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ★
+
+ ))}
+
+
+ ({book.rating}/5)
+
+
+
+
+ {book.category}
+
+
+ {book.year}
+
+
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/components/layouts/ProjectLayout.tsx b/app/components/layouts/ProjectLayout.tsx
new file mode 100644
index 0000000..a298517
--- /dev/null
+++ b/app/components/layouts/ProjectLayout.tsx
@@ -0,0 +1,84 @@
+import { Link } from "@remix-run/react";
+import { RootLayout } from "./RootLayout";
+import { type EmbedConsent } from "~/utils/embed-consent";
+import { getMDXComponents } from "../mdx";
+import { useMemo } from "react";
+import { RelatedContent } from "../RelatedContent";
+import { findRelatedContent } from "~/utils/content-relations";
+
+interface ProjectLayoutProps {
+ title: string;
+ description: string;
+ tags: string[];
+ image?: string;
+ slug: string;
+ content: string;
+ embedConsent: EmbedConsent;
+}
+
+export function ProjectLayout({
+ title,
+ description,
+ tags,
+ image,
+ slug,
+ content,
+ embedConsent,
+}: ProjectLayoutProps) {
+ const components = useMemo(
+ () => getMDXComponents({ embedConsent }),
+ [embedConsent],
+ );
+
+ const relatedContent = findRelatedContent("project", slug, tags);
+
+ return (
+
+
+
+
+ ← Back to projects
+
+
+
+
+
+ {title}
+ {description}
+ {tags.length > 0 && (
+
+ {tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+
+ {image && (
+
+
+
+ )}
+
+ {content}
+
+
+
+
+
+ );
+}
diff --git a/app/components/layouts/ProjectListLayout.tsx b/app/components/layouts/ProjectListLayout.tsx
new file mode 100644
index 0000000..0809bef
--- /dev/null
+++ b/app/components/layouts/ProjectListLayout.tsx
@@ -0,0 +1,75 @@
+import { Link } from "@remix-run/react";
+import { RootLayout } from "./RootLayout";
+
+interface Project {
+ title: string;
+ description: string;
+ slug: string;
+ tags: string[];
+ image?: string;
+}
+
+interface ProjectListLayoutProps {
+ projects: Project[];
+}
+
+export function ProjectListLayout({ projects }: ProjectListLayoutProps) {
+ return (
+
+
+
+
Projects
+
+ Here are some of the projects I've worked on. Most of them are open
+ source and available on GitHub.
+
+
+
+
+ {projects.map((project) => (
+
+
+ {project.image && (
+
+
+
+ )}
+
+
+ {project.title}
+
+
{project.description}
+ {project.tags.length > 0 && (
+
+ {project.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/components/layouts/RootLayout.tsx b/app/components/layouts/RootLayout.tsx
new file mode 100644
index 0000000..0359810
--- /dev/null
+++ b/app/components/layouts/RootLayout.tsx
@@ -0,0 +1,79 @@
+import { type PropsWithChildren, useState } from "react";
+import { type ActiveTabType } from "~/utils/navigation";
+import { BaseLayout } from "./BaseLayout";
+import { Header } from "../Header";
+import { Footer } from "../Footer";
+import clsx from "clsx";
+
+interface RootLayoutProps extends PropsWithChildren {
+ title: string;
+ description: string;
+ activeTab: ActiveTabType;
+}
+
+export function RootLayout({
+ children,
+ title,
+ description,
+ activeTab,
+}: RootLayoutProps) {
+ const [isSidebarExpanded, setIsSidebarExpanded] = useState(true);
+
+ return (
+
+
+
+ setIsSidebarExpanded(!isSidebarExpanded)}
+ className="absolute left-3 top-3 hidden rounded-md p-1 text-gray-500 lg:block notouch:hover:bg-slate-200 notouch:hover:text-gray-900"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/components/mdx/index.tsx b/app/components/mdx/index.tsx
new file mode 100644
index 0000000..c345f99
--- /dev/null
+++ b/app/components/mdx/index.tsx
@@ -0,0 +1,133 @@
+import { Link } from "@remix-run/react";
+import { useMemo } from "react";
+import { type EmbedConsent } from "~/utils/embed-consent";
+import {
+ YouTubeEmbed,
+ TwitterEmbed,
+ InstagramEmbed,
+ RedditEmbed,
+} from "../embeds";
+
+interface MDXComponentsProps {
+ embedConsent: EmbedConsent;
+}
+
+export function getMDXComponents({ embedConsent }: MDXComponentsProps) {
+ return useMemo(
+ () => ({
+ // Custom link handling
+ a: ({
+ href,
+ children,
+ ...props
+ }: React.AnchorHTMLAttributes) => {
+ const isExternal = href?.startsWith("http");
+ const isAnchor = href?.startsWith("#");
+
+ if (isExternal) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ if (isAnchor) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+ },
+
+ // Headings with anchor links
+ h2: ({ id, children }: React.HTMLAttributes) => (
+
+
+ #
+
+ {children}
+
+ ),
+
+ h3: ({ id, children }: React.HTMLAttributes) => (
+
+
+ #
+
+ {children}
+
+ ),
+
+ // Code blocks
+ pre: ({ children, ...props }: React.HTMLAttributes) => (
+
+ {children}
+
+ ),
+
+ code: ({ children }: React.HTMLAttributes) => (
+
+ {children}
+
+ ),
+
+ // Embeds
+ YouTubeEmbed: ({ id, title }: { id: string; title?: string }) => (
+
+ ),
+
+ TwitterEmbed: ({ id }: { id: string }) => (
+
+ ),
+
+ InstagramEmbed: ({ id }: { id: string }) => (
+
+ ),
+
+ RedditEmbed: ({ url }: { url: string }) => (
+
+ ),
+ }),
+ [embedConsent],
+ );
+}
diff --git a/app/data/socials.ts b/app/data/socials.ts
new file mode 100644
index 0000000..7318eb0
--- /dev/null
+++ b/app/data/socials.ts
@@ -0,0 +1,25 @@
+export type SocialType =
+ | "twitter"
+ | "github"
+ | "youtube"
+ | "instagram"
+ | "linkedin";
+
+export const socials = {
+ twitter: "https://twitter.com/chrcit",
+ github: "https://github.com/chrcit",
+ youtube: "https://www.youtube.com/@chrcit",
+ instagram: "https://instagram.com/chrcit",
+ linkedin: "https://linkedin.com/in/chrcit",
+} as const;
+
+export const socialLinks: {
+ type: SocialType;
+ href: string;
+}[] = [
+ { type: "twitter", href: socials.twitter },
+ { type: "github", href: socials.github },
+ { type: "youtube", href: socials.youtube },
+ { type: "instagram", href: socials.instagram },
+ { type: "linkedin", href: socials.linkedin },
+];
diff --git a/app/entry.client.tsx b/app/entry.client.tsx
new file mode 100644
index 0000000..74a69e2
--- /dev/null
+++ b/app/entry.client.tsx
@@ -0,0 +1,7 @@
+import { RemixBrowser } from "@remix-run/react";
+import { startTransition } from "react";
+import { hydrateRoot } from "react-dom/client";
+
+startTransition(() => {
+ hydrateRoot(document, );
+});
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
new file mode 100644
index 0000000..10a0eb6
--- /dev/null
+++ b/app/entry.server.tsx
@@ -0,0 +1,129 @@
+import { PassThrough } from "node:stream";
+import type { EntryContext } from "@remix-run/node";
+import { createReadableStreamFromReadable } from "@remix-run/node";
+import { RemixServer } from "@remix-run/react";
+import { isbot } from "isbot";
+import { renderToPipeableStream } from "react-dom/server";
+
+const ABORT_DELAY = 5_000;
+
+export default function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext,
+) {
+ return isbot(request.headers.get("user-agent"))
+ ? handleBotRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext,
+ )
+ : handleBrowserRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext,
+ );
+}
+
+function handleBotRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext,
+) {
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ onAllReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set("Content-Type", "text/html");
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ }),
+ );
+
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ },
+ );
+
+ setTimeout(abort, ABORT_DELAY);
+ });
+}
+
+function handleBrowserRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext,
+) {
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ onShellReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set("Content-Type", "text/html");
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ }),
+ );
+
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ },
+ );
+
+ setTimeout(abort, ABORT_DELAY);
+ });
+}
diff --git a/app/hooks/useFocusManagement.ts b/app/hooks/useFocusManagement.ts
new file mode 100644
index 0000000..3080acf
--- /dev/null
+++ b/app/hooks/useFocusManagement.ts
@@ -0,0 +1,65 @@
+import { useEffect, useRef } from "react";
+import { useLocation } from "@remix-run/react";
+
+export function useFocusManagement() {
+ const location = useLocation();
+ const lastFocusedElement = useRef(null);
+
+ useEffect(() => {
+ // Save the currently focused element before route change
+ lastFocusedElement.current = document.activeElement as HTMLElement;
+
+ // Focus the main content after route change
+ const mainContent = document.querySelector("main");
+ if (mainContent) {
+ // Set tabindex to make the element focusable
+ mainContent.setAttribute("tabindex", "-1");
+ mainContent.focus();
+ // Remove tabindex after focus to prevent keyboard navigation issues
+ mainContent.removeAttribute("tabindex");
+ }
+
+ // Restore focus when component unmounts
+ return () => {
+ if (lastFocusedElement.current) {
+ lastFocusedElement.current.focus();
+ }
+ };
+ }, [location.pathname]);
+
+ // Handle modal focus trap
+ const trapFocus = (element: HTMLElement) => {
+ const focusableElements = element.querySelectorAll(
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
+ );
+ const firstFocusable = focusableElements[0] as HTMLElement;
+ const lastFocusable = focusableElements[
+ focusableElements.length - 1
+ ] as HTMLElement;
+
+ function handleTabKey(e: KeyboardEvent) {
+ if (e.key !== "Tab") return;
+
+ if (e.shiftKey) {
+ if (document.activeElement === firstFocusable) {
+ e.preventDefault();
+ lastFocusable.focus();
+ }
+ } else {
+ if (document.activeElement === lastFocusable) {
+ e.preventDefault();
+ firstFocusable.focus();
+ }
+ }
+ }
+
+ element.addEventListener("keydown", handleTabKey);
+ firstFocusable.focus();
+
+ return () => {
+ element.removeEventListener("keydown", handleTabKey);
+ };
+ };
+
+ return { trapFocus };
+}
diff --git a/app/hooks/useKeyboardNavigation.ts b/app/hooks/useKeyboardNavigation.ts
new file mode 100644
index 0000000..fe4b14e
--- /dev/null
+++ b/app/hooks/useKeyboardNavigation.ts
@@ -0,0 +1,71 @@
+import { useEffect } from "react";
+import { useNavigate } from "@remix-run/react";
+
+export function useKeyboardNavigation() {
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ function handleKeyDown(event: KeyboardEvent) {
+ // Only handle keyboard shortcuts when not in an input or textarea
+ if (
+ event.target instanceof HTMLInputElement ||
+ event.target instanceof HTMLTextAreaElement
+ ) {
+ return;
+ }
+
+ // Handle keyboard shortcuts
+ switch (event.key) {
+ case "h":
+ if (event.metaKey || event.ctrlKey) {
+ event.preventDefault();
+ navigate("/");
+ }
+ break;
+ case "a":
+ if (event.metaKey || event.ctrlKey) {
+ event.preventDefault();
+ navigate("/articles");
+ }
+ break;
+ case "p":
+ if (event.metaKey || event.ctrlKey) {
+ event.preventDefault();
+ navigate("/projects");
+ }
+ break;
+ case "b":
+ if (event.metaKey || event.ctrlKey) {
+ event.preventDefault();
+ navigate("/books");
+ }
+ break;
+ case "ArrowLeft":
+ if (event.altKey) {
+ event.preventDefault();
+ window.history.back();
+ }
+ break;
+ case "ArrowRight":
+ if (event.altKey) {
+ event.preventDefault();
+ window.history.forward();
+ }
+ break;
+ case "/":
+ event.preventDefault();
+ // Focus search input if it exists
+ const searchInput = document.querySelector(
+ 'input[type="search"]',
+ ) as HTMLInputElement;
+ if (searchInput) {
+ searchInput.focus();
+ }
+ break;
+ }
+ }
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [navigate]);
+}
diff --git a/app/root.tsx b/app/root.tsx
new file mode 100644
index 0000000..d02a89b
--- /dev/null
+++ b/app/root.tsx
@@ -0,0 +1,118 @@
+import { cssBundleHref } from "@remix-run/css-bundle";
+import {
+ json,
+ type LinksFunction,
+ type LoaderFunctionArgs,
+ type MetaFunction,
+} from "@remix-run/node";
+import {
+ Links,
+ LiveReload,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+ useLoaderData,
+ useLocation,
+ useMatches,
+} from "@remix-run/react";
+import { useEffect } from "react";
+
+import tailwindStyles from "~/styles/tailwind.css";
+import { getTheme, setTheme } from "~/utils/theme.server";
+
+export const links: LinksFunction = () => [
+ { rel: "stylesheet", href: tailwindStyles },
+ ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
+ { rel: "preconnect", href: "https://fonts.googleapis.com" },
+ { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "" },
+ {
+ rel: "stylesheet",
+ href: "https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&family=Playfair+Display:wght@700&family=Schibsted+Grotesk:wght@400;500;600;700&display=swap",
+ },
+];
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: "Christian Cito" },
+ { name: "description", content: "Personal website of Christian Cito" },
+ { name: "viewport", content: "width=device-width,initial-scale=1" },
+ { name: "theme-color", content: "#000000" },
+ { property: "og:type", content: "website" },
+ { property: "og:site_name", content: "Christian Cito" },
+ ];
+};
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ return json({
+ theme: await getTheme(request),
+ });
+}
+
+function prefetchNextRoutes() {
+ const matches = useMatches();
+ const location = useLocation();
+
+ useEffect(() => {
+ // Prefetch next likely routes based on current route
+ let routesToPrefetch: string[] = [];
+
+ if (location.pathname === "/") {
+ routesToPrefetch = ["/articles", "/projects", "/books"];
+ } else if (location.pathname === "/articles") {
+ routesToPrefetch = ["/projects", "/books"];
+ } else if (location.pathname === "/projects") {
+ routesToPrefetch = ["/articles", "/books"];
+ } else if (location.pathname === "/books") {
+ routesToPrefetch = ["/articles", "/projects"];
+ }
+
+ // Use requestIdleCallback to prefetch during idle time
+ const handle = window.requestIdleCallback(() => {
+ routesToPrefetch.forEach((route) => {
+ const link = document.createElement("link");
+ link.rel = "prefetch";
+ link.href = route;
+ document.head.appendChild(link);
+ });
+ });
+
+ return () => {
+ window.cancelIdleCallback(handle);
+ };
+ }, [location.pathname]);
+}
+
+export default function App() {
+ const { theme } = useLoaderData();
+ prefetchNextRoutes();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function SkipToContent() {
+ return (
+
+ Skip to content
+
+ );
+}
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
new file mode 100644
index 0000000..4573067
--- /dev/null
+++ b/app/routes/_index.tsx
@@ -0,0 +1,35 @@
+import { json } from "@remix-run/node";
+import type { MetaFunction } from "@remix-run/node";
+import { useLoaderData } from "@remix-run/react";
+import { RootLayout } from "~/components/layouts/RootLayout";
+import { Particles } from "~/components/Particles";
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: "Christian Cito" },
+ { name: "description", content: "Personal website of Christian Cito" },
+ ];
+};
+
+export async function loader() {
+ return json({
+ title: "Christian Cito",
+ description: "Personal website of Christian Cito",
+ });
+}
+
+export default function Index() {
+ const { title, description } = useLoaderData();
+
+ return (
+
+
+
+
+
{title}
+
{description}
+
+
+
+ );
+}
diff --git a/app/routes/api.embed-consent.ts b/app/routes/api.embed-consent.ts
new file mode 100644
index 0000000..a567972
--- /dev/null
+++ b/app/routes/api.embed-consent.ts
@@ -0,0 +1,27 @@
+import { json, type ActionFunctionArgs } from "@remix-run/node";
+import { updateEmbedConsent } from "~/utils/embed-consent";
+
+export async function action({ request }: ActionFunctionArgs) {
+ if (request.method !== "POST") {
+ return json({ error: "Method not allowed" }, { status: 405 });
+ }
+
+ try {
+ const formData = await request.formData();
+ const consent = Object.fromEntries(formData.entries());
+
+ // Convert string values to booleans
+ const parsedConsent = Object.entries(consent).reduce(
+ (acc, [key, value]) => ({
+ ...acc,
+ [key]: value === "true",
+ }),
+ {},
+ );
+
+ return await updateEmbedConsent(request, parsedConsent);
+ } catch (error) {
+ console.error("Error updating embed consent:", error);
+ return json({ error: "Failed to update embed consent" }, { status: 500 });
+ }
+}
diff --git a/app/routes/articles.$slug.tsx b/app/routes/articles.$slug.tsx
new file mode 100644
index 0000000..0bae00a
--- /dev/null
+++ b/app/routes/articles.$slug.tsx
@@ -0,0 +1,33 @@
+import { json, type LoaderFunctionArgs } from "@remix-run/node";
+import { useLoaderData } from "@remix-run/react";
+import { ArticleLayout } from "~/components/layouts/ArticleLayout";
+import { getArticle } from "~/utils/content";
+import { getEmbedConsent } from "~/utils/embed-consent";
+
+export async function loader({ params, request }: LoaderFunctionArgs) {
+ const article = getArticle(params.slug ?? "");
+ if (!article) {
+ throw new Response("Not Found", { status: 404 });
+ }
+
+ const embedConsent = await getEmbedConsent(request);
+
+ return json({
+ article,
+ embedConsent,
+ });
+}
+
+export default function ArticlePage() {
+ const { article, embedConsent } = useLoaderData();
+
+ return (
+
+ );
+}
diff --git a/app/routes/articles._index.tsx b/app/routes/articles._index.tsx
new file mode 100644
index 0000000..adf725b
--- /dev/null
+++ b/app/routes/articles._index.tsx
@@ -0,0 +1,14 @@
+import { json } from "@remix-run/node";
+import { useLoaderData } from "@remix-run/react";
+import { ArticleListLayout } from "~/components/layouts/ArticleListLayout";
+import { getArticles } from "~/utils/content";
+
+export function loader() {
+ const articles = getArticles();
+ return json({ articles });
+}
+
+export default function ArticlesPage() {
+ const { articles } = useLoaderData();
+ return ;
+}
diff --git a/app/routes/books.$slug.tsx b/app/routes/books.$slug.tsx
new file mode 100644
index 0000000..02b8370
--- /dev/null
+++ b/app/routes/books.$slug.tsx
@@ -0,0 +1,39 @@
+import { json, type LoaderFunctionArgs } from "@remix-run/node";
+import { useLoaderData } from "@remix-run/react";
+import { BookLayout } from "~/components/layouts/BookLayout";
+import { getBook } from "~/utils/content";
+import { getEmbedConsent } from "~/utils/embed-consent";
+
+export async function loader({ params, request }: LoaderFunctionArgs) {
+ const book = getBook(params.slug ?? "");
+ if (!book) {
+ throw new Response("Not Found", { status: 404 });
+ }
+
+ const embedConsent = await getEmbedConsent(request);
+
+ return json({
+ book,
+ embedConsent,
+ });
+}
+
+export default function BookPage() {
+ const { book, embedConsent } = useLoaderData();
+
+ return (
+
+ );
+}
diff --git a/app/routes/books._index.tsx b/app/routes/books._index.tsx
new file mode 100644
index 0000000..f202380
--- /dev/null
+++ b/app/routes/books._index.tsx
@@ -0,0 +1,14 @@
+import { json } from "@remix-run/node";
+import { useLoaderData } from "@remix-run/react";
+import { BookListLayout } from "~/components/layouts/BookListLayout";
+import { getBooks } from "~/utils/content";
+
+export function loader() {
+ const books = getBooks();
+ return json({ books });
+}
+
+export default function BooksPage() {
+ const { books } = useLoaderData();
+ return ;
+}
diff --git a/app/routes/feed[.]xml.ts b/app/routes/feed[.]xml.ts
new file mode 100644
index 0000000..5f2f4cf
--- /dev/null
+++ b/app/routes/feed[.]xml.ts
@@ -0,0 +1,41 @@
+import { type LoaderFunctionArgs } from "@remix-run/node";
+import { getArticles } from "~/utils/content";
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const url = new URL(request.url);
+ const baseUrl = `${url.protocol}//${url.host}`;
+
+ const articles = getArticles();
+
+ const feed = `
+
+
+ Christian Cito
+ Personal website of Christian Cito
+ ${baseUrl}
+
+ en-US
+ ${articles
+ .map(
+ (article) => `
+ -
+
${article.title}
+ ${article.description}
+ ${new Date(article.publishedAt).toUTCString()}
+ ${baseUrl}/articles/${article.slug}
+ ${baseUrl}/articles/${article.slug}
+ ${article.tags.map((tag) => `${tag} `).join("")}
+ `,
+ )
+ .join("")}
+
+ `;
+
+ return new Response(feed, {
+ headers: {
+ "Content-Type": "application/xml",
+ "Content-Length": String(Buffer.byteLength(feed)),
+ "Cache-Control": "public, max-age=3600",
+ },
+ });
+}
diff --git a/app/routes/projects.$slug.tsx b/app/routes/projects.$slug.tsx
new file mode 100644
index 0000000..cb75142
--- /dev/null
+++ b/app/routes/projects.$slug.tsx
@@ -0,0 +1,34 @@
+import { json, type LoaderFunctionArgs } from "@remix-run/node";
+import { useLoaderData } from "@remix-run/react";
+import { ProjectLayout } from "~/components/layouts/ProjectLayout";
+import { getProject } from "~/utils/content";
+import { getEmbedConsent } from "~/utils/embed-consent";
+
+export async function loader({ params, request }: LoaderFunctionArgs) {
+ const project = getProject(params.slug ?? "");
+ if (!project) {
+ throw new Response("Not Found", { status: 404 });
+ }
+
+ const embedConsent = await getEmbedConsent(request);
+
+ return json({
+ project,
+ embedConsent,
+ });
+}
+
+export default function ProjectPage() {
+ const { project, embedConsent } = useLoaderData();
+
+ return (
+
+ );
+}
diff --git a/app/routes/projects._index.tsx b/app/routes/projects._index.tsx
new file mode 100644
index 0000000..d2a4de3
--- /dev/null
+++ b/app/routes/projects._index.tsx
@@ -0,0 +1,14 @@
+import { json } from "@remix-run/node";
+import { useLoaderData } from "@remix-run/react";
+import { ProjectListLayout } from "~/components/layouts/ProjectListLayout";
+import { getProjects } from "~/utils/content";
+
+export function loader() {
+ const projects = getProjects();
+ return json({ projects });
+}
+
+export default function ProjectsPage() {
+ const { projects } = useLoaderData();
+ return ;
+}
diff --git a/app/routes/sitemap[.]xml.ts b/app/routes/sitemap[.]xml.ts
new file mode 100644
index 0000000..604ea6f
--- /dev/null
+++ b/app/routes/sitemap[.]xml.ts
@@ -0,0 +1,74 @@
+import { type LoaderFunctionArgs } from "@remix-run/node";
+import { getArticles, getBooks, getProjects } from "~/utils/content";
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const url = new URL(request.url);
+ const baseUrl = `${url.protocol}//${url.host}`;
+
+ const articles = getArticles();
+ const books = getBooks();
+ const projects = getProjects();
+
+ const sitemap = `
+
+
+ ${baseUrl}
+ weekly
+ 1.0
+
+
+ ${baseUrl}/articles
+ weekly
+ 0.8
+
+
+ ${baseUrl}/projects
+ monthly
+ 0.8
+
+
+ ${baseUrl}/books
+ monthly
+ 0.8
+
+ ${articles
+ .map(
+ (article) => `
+
+ ${baseUrl}/articles/${article.slug}
+ ${article.publishedAt}
+ monthly
+ 0.6
+ `,
+ )
+ .join("")}
+ ${projects
+ .map(
+ (project) => `
+
+ ${baseUrl}/projects/${project.slug}
+ monthly
+ 0.6
+ `,
+ )
+ .join("")}
+ ${books
+ .map(
+ (book) => `
+
+ ${baseUrl}/books/${book.slug}
+ monthly
+ 0.6
+ `,
+ )
+ .join("")}
+ `;
+
+ return new Response(sitemap, {
+ headers: {
+ "Content-Type": "application/xml",
+ "Content-Length": String(Buffer.byteLength(sitemap)),
+ "Cache-Control": "public, max-age=3600",
+ },
+ });
+}
diff --git a/app/styles/tailwind.css b/app/styles/tailwind.css
new file mode 100644
index 0000000..74cf100
--- /dev/null
+++ b/app/styles/tailwind.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ html {
+ @apply scroll-smooth;
+ }
+
+ body {
+ @apply bg-white text-gray-900;
+ }
+}
diff --git a/app/utils/content-relations.ts b/app/utils/content-relations.ts
new file mode 100644
index 0000000..1041e89
--- /dev/null
+++ b/app/utils/content-relations.ts
@@ -0,0 +1,92 @@
+import { getArticles, getBooks, getProjects } from "./content";
+
+interface RelatedContent {
+ title: string;
+ description: string;
+ slug: string;
+ type: "article" | "book" | "project";
+ tags: string[];
+}
+
+export function findRelatedContent(
+ type: "article" | "book" | "project",
+ slug: string,
+ tags: string[],
+ limit = 3,
+): RelatedContent[] {
+ const articles = getArticles();
+ const books = getBooks();
+ const projects = getProjects();
+
+ // Exclude the current content
+ const allContent: RelatedContent[] = [
+ ...articles
+ .filter((article) => article.slug !== slug)
+ .map((article) => ({
+ title: article.title,
+ description: article.description,
+ slug: article.slug,
+ type: "article" as const,
+ tags: article.tags,
+ })),
+ ...books
+ .filter((book) => book.slug !== slug)
+ .map((book) => ({
+ title: book.title,
+ description: book.description,
+ slug: book.slug,
+ type: "book" as const,
+ tags: book.tags,
+ })),
+ ...projects
+ .filter((project) => project.slug !== slug)
+ .map((project) => ({
+ title: project.title,
+ description: project.description,
+ slug: project.slug,
+ type: "project" as const,
+ tags: project.tags,
+ })),
+ ];
+
+ // Calculate relevance score based on tag matches
+ const scoredContent = allContent.map((content) => {
+ const matchingTags = tags.filter((tag) =>
+ content.tags.includes(tag),
+ ).length;
+ return {
+ ...content,
+ score: matchingTags,
+ };
+ });
+
+ // Sort by score and get top matches
+ return scoredContent
+ .sort((a, b) => b.score - a.score)
+ .slice(0, limit)
+ .map(({ title, description, slug, type, tags }) => ({
+ title,
+ description,
+ slug,
+ type,
+ tags,
+ }));
+}
+
+export function getRelatedArticles(slug: string, tags: string[], limit = 3) {
+ return findRelatedContent("article", slug, tags, limit).filter(
+ (content) => content.type === "article",
+ );
+}
+
+export function getRelatedBooks(slug: string, tags: string[], limit = 3) {
+ return findRelatedContent("book", slug, tags, limit).filter(
+ (content) => content.type === "book",
+ );
+}
+
+export function getRelatedProjects(slug: string, tags: string[], limit = 3) {
+ return findRelatedContent("project", slug, tags, limit).filter(
+ (content) => content.type === "project",
+ );
+}
diff --git a/app/utils/content.ts b/app/utils/content.ts
new file mode 100644
index 0000000..45c9577
--- /dev/null
+++ b/app/utils/content.ts
@@ -0,0 +1,38 @@
+import {
+ allArticles,
+ allBooks,
+ allPages,
+ allProjects,
+} from "contentlayer/generated";
+
+export function getArticles() {
+ return allArticles.sort(
+ (a, b) =>
+ new Date(b.publishedAt ?? 0).getTime() -
+ new Date(a.publishedAt ?? 0).getTime(),
+ );
+}
+
+export function getArticle(slug: string) {
+ return allArticles.find((article) => article.slug === slug);
+}
+
+export function getProjects() {
+ return allProjects.sort((a, b) => a.order - b.order);
+}
+
+export function getProject(slug: string) {
+ return allProjects.find((project) => project.slug === slug);
+}
+
+export function getBooks() {
+ return allBooks.sort((a, b) => b.year - a.year);
+}
+
+export function getBook(slug: string) {
+ return allBooks.find((book) => book.slug === slug);
+}
+
+export function getPage(slug: string) {
+ return allPages.find((page) => page.slug === slug);
+}
diff --git a/app/utils/embed-consent.ts b/app/utils/embed-consent.ts
new file mode 100644
index 0000000..d562741
--- /dev/null
+++ b/app/utils/embed-consent.ts
@@ -0,0 +1,47 @@
+import { createCookie } from "@remix-run/node";
+
+export type EmbedType = "youtube" | "twitter" | "instagram" | "reddit";
+
+export interface EmbedConsent {
+ youtube: boolean;
+ twitter: boolean;
+ instagram: boolean;
+ reddit: boolean;
+}
+
+const embedConsentCookie = createCookie("embed-consent", {
+ path: "/",
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ maxAge: 31536000, // one year
+});
+
+export async function getEmbedConsent(request: Request): Promise {
+ const cookieHeader = request.headers.get("Cookie");
+ const consent = await embedConsentCookie.parse(cookieHeader);
+ return {
+ youtube: consent?.youtube ?? false,
+ twitter: consent?.twitter ?? false,
+ instagram: consent?.instagram ?? false,
+ reddit: consent?.reddit ?? false,
+ };
+}
+
+export async function setEmbedConsent(consent: Partial) {
+ return await embedConsentCookie.serialize(consent);
+}
+
+export async function updateEmbedConsent(
+ request: Request,
+ consent: Partial,
+) {
+ const currentConsent = await getEmbedConsent(request);
+ const newConsent = { ...currentConsent, ...consent };
+ return new Response(null, {
+ status: 200,
+ headers: {
+ "Set-Cookie": await setEmbedConsent(newConsent),
+ },
+ });
+}
diff --git a/app/utils/navigation.ts b/app/utils/navigation.ts
new file mode 100644
index 0000000..b2e7077
--- /dev/null
+++ b/app/utils/navigation.ts
@@ -0,0 +1,19 @@
+export type ActiveTabType = "home" | "articles" | "projects" | "books";
+
+export const navigationLinks = [
+ {
+ name: "Articles",
+ href: "/articles",
+ activeTab: "articles" as const,
+ },
+ {
+ name: "Projects",
+ href: "/projects",
+ activeTab: "projects" as const,
+ },
+ {
+ name: "Books",
+ href: "/books",
+ activeTab: "books" as const,
+ },
+];
diff --git a/app/utils/structured-data.ts b/app/utils/structured-data.ts
new file mode 100644
index 0000000..97c644e
--- /dev/null
+++ b/app/utils/structured-data.ts
@@ -0,0 +1,85 @@
+interface Article {
+ title: string;
+ description: string;
+ publishedAt: string;
+ updatedAt?: string;
+ author: string;
+ url: string;
+ image?: string;
+ tags: string[];
+}
+
+interface Book {
+ title: string;
+ description: string;
+ author: string;
+ rating: number;
+ cover: string;
+ year: number;
+ category: string;
+ tags: string[];
+ url: string;
+}
+
+interface Project {
+ title: string;
+ description: string;
+ image?: string;
+ url: string;
+ tags: string[];
+}
+
+export function generateArticleStructuredData(article: Article) {
+ return {
+ "@context": "https://schema.org",
+ "@type": "BlogPosting",
+ headline: article.title,
+ description: article.description,
+ author: {
+ "@type": "Person",
+ name: article.author,
+ },
+ datePublished: article.publishedAt,
+ dateModified: article.updatedAt || article.publishedAt,
+ image: article.image,
+ url: article.url,
+ keywords: article.tags.join(", "),
+ };
+}
+
+export function generateBookStructuredData(book: Book) {
+ return {
+ "@context": "https://schema.org",
+ "@type": "Book",
+ name: book.title,
+ description: book.description,
+ author: {
+ "@type": "Person",
+ name: book.author,
+ },
+ datePublished: book.year.toString(),
+ image: book.cover,
+ url: book.url,
+ genre: book.category,
+ keywords: book.tags.join(", "),
+ aggregateRating: {
+ "@type": "AggregateRating",
+ ratingValue: book.rating,
+ bestRating: "5",
+ ratingCount: "1",
+ },
+ };
+}
+
+export function generateProjectStructuredData(project: Project) {
+ return {
+ "@context": "https://schema.org",
+ "@type": "SoftwareApplication",
+ name: project.title,
+ description: project.description,
+ image: project.image,
+ url: project.url,
+ applicationCategory: "WebApplication",
+ keywords: project.tags.join(", "),
+ };
+}
diff --git a/app/utils/theme.server.ts b/app/utils/theme.server.ts
new file mode 100644
index 0000000..8904734
--- /dev/null
+++ b/app/utils/theme.server.ts
@@ -0,0 +1,30 @@
+import { createCookie } from "@remix-run/node";
+
+const themeCookie = createCookie("theme", {
+ path: "/",
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ maxAge: 31536000, // one year
+});
+
+export type Theme = "light" | "dark" | "system";
+
+export async function getTheme(request: Request): Promise {
+ const cookieHeader = request.headers.get("Cookie");
+ const theme = await themeCookie.parse(cookieHeader);
+ return theme?.theme || "system";
+}
+
+export async function setTheme(theme: Theme) {
+ return await themeCookie.serialize({ theme });
+}
+
+export async function updateTheme(request: Request, theme: Theme) {
+ return new Response(null, {
+ status: 200,
+ headers: {
+ "Set-Cookie": await setTheme(theme),
+ },
+ });
+}
diff --git a/astro.config.mjs b/astro-site/astro.config.mjs
similarity index 100%
rename from astro.config.mjs
rename to astro-site/astro.config.mjs
diff --git a/package-lock.json b/astro-site/package-lock.json
similarity index 100%
rename from package-lock.json
rename to astro-site/package-lock.json
diff --git a/src/components/BackButton.astro b/astro-site/src/components/BackButton.astro
similarity index 100%
rename from src/components/BackButton.astro
rename to astro-site/src/components/BackButton.astro
diff --git a/src/components/ExternalLink.astro b/astro-site/src/components/ExternalLink.astro
similarity index 100%
rename from src/components/ExternalLink.astro
rename to astro-site/src/components/ExternalLink.astro
diff --git a/src/components/Footer.astro b/astro-site/src/components/Footer.astro
similarity index 100%
rename from src/components/Footer.astro
rename to astro-site/src/components/Footer.astro
diff --git a/src/components/Header.astro b/astro-site/src/components/Header.astro
similarity index 100%
rename from src/components/Header.astro
rename to astro-site/src/components/Header.astro
diff --git a/src/components/ListHeader.astro b/astro-site/src/components/ListHeader.astro
similarity index 100%
rename from src/components/ListHeader.astro
rename to astro-site/src/components/ListHeader.astro
diff --git a/src/components/Particles.astro b/astro-site/src/components/Particles.astro
similarity index 100%
rename from src/components/Particles.astro
rename to astro-site/src/components/Particles.astro
diff --git a/src/components/ReadMoreButton.astro b/astro-site/src/components/ReadMoreButton.astro
similarity index 100%
rename from src/components/ReadMoreButton.astro
rename to astro-site/src/components/ReadMoreButton.astro
diff --git a/src/components/SkipToContent.astro b/astro-site/src/components/SkipToContent.astro
similarity index 100%
rename from src/components/SkipToContent.astro
rename to astro-site/src/components/SkipToContent.astro
diff --git a/src/components/SocialIcon.astro b/astro-site/src/components/SocialIcon.astro
similarity index 100%
rename from src/components/SocialIcon.astro
rename to astro-site/src/components/SocialIcon.astro
diff --git a/src/components/SocialLinks.astro b/astro-site/src/components/SocialLinks.astro
similarity index 100%
rename from src/components/SocialLinks.astro
rename to astro-site/src/components/SocialLinks.astro
diff --git a/src/components/TableOfContents.astro b/astro-site/src/components/TableOfContents.astro
similarity index 100%
rename from src/components/TableOfContents.astro
rename to astro-site/src/components/TableOfContents.astro
diff --git a/src/components/TableOfContentsHeading.astro b/astro-site/src/components/TableOfContentsHeading.astro
similarity index 100%
rename from src/components/TableOfContentsHeading.astro
rename to astro-site/src/components/TableOfContentsHeading.astro
diff --git a/src/components/embeds/AndererseitsInstagram.astro b/astro-site/src/components/embeds/AndererseitsInstagram.astro
similarity index 100%
rename from src/components/embeds/AndererseitsInstagram.astro
rename to astro-site/src/components/embeds/AndererseitsInstagram.astro
diff --git a/src/components/icons/ArrowLeft.astro b/astro-site/src/components/icons/ArrowLeft.astro
similarity index 100%
rename from src/components/icons/ArrowLeft.astro
rename to astro-site/src/components/icons/ArrowLeft.astro
diff --git a/src/components/icons/ArrowRight.astro b/astro-site/src/components/icons/ArrowRight.astro
similarity index 100%
rename from src/components/icons/ArrowRight.astro
rename to astro-site/src/components/icons/ArrowRight.astro
diff --git a/src/components/particles.json b/astro-site/src/components/particles.json
similarity index 100%
rename from src/components/particles.json
rename to astro-site/src/components/particles.json
diff --git a/src/content/articles/2023-year-in-review.mdx b/astro-site/src/content/articles/2023-year-in-review.mdx
similarity index 100%
rename from src/content/articles/2023-year-in-review.mdx
rename to astro-site/src/content/articles/2023-year-in-review.mdx
diff --git a/src/content/articles/images/der-wanderer-ueber-dem-nebelmeer.jpg b/astro-site/src/content/articles/images/der-wanderer-ueber-dem-nebelmeer.jpg
similarity index 100%
rename from src/content/articles/images/der-wanderer-ueber-dem-nebelmeer.jpg
rename to astro-site/src/content/articles/images/der-wanderer-ueber-dem-nebelmeer.jpg
diff --git a/src/content/books/a-liberated-mind.md b/astro-site/src/content/books/a-liberated-mind.md
similarity index 100%
rename from src/content/books/a-liberated-mind.md
rename to astro-site/src/content/books/a-liberated-mind.md
diff --git a/src/content/books/antifragile.md b/astro-site/src/content/books/antifragile.md
similarity index 100%
rename from src/content/books/antifragile.md
rename to astro-site/src/content/books/antifragile.md
diff --git a/src/content/books/black-swan.md b/astro-site/src/content/books/black-swan.md
similarity index 100%
rename from src/content/books/black-swan.md
rename to astro-site/src/content/books/black-swan.md
diff --git a/src/content/books/brave-new-world.md b/astro-site/src/content/books/brave-new-world.md
similarity index 100%
rename from src/content/books/brave-new-world.md
rename to astro-site/src/content/books/brave-new-world.md
diff --git a/src/content/books/breaking-out-of-homeostasis.md b/astro-site/src/content/books/breaking-out-of-homeostasis.md
similarity index 100%
rename from src/content/books/breaking-out-of-homeostasis.md
rename to astro-site/src/content/books/breaking-out-of-homeostasis.md
diff --git a/src/content/books/capitalist-realism.md b/astro-site/src/content/books/capitalist-realism.md
similarity index 100%
rename from src/content/books/capitalist-realism.md
rename to astro-site/src/content/books/capitalist-realism.md
diff --git a/src/content/books/covers/a-liberated-mind-cover.jpg b/astro-site/src/content/books/covers/a-liberated-mind-cover.jpg
similarity index 100%
rename from src/content/books/covers/a-liberated-mind-cover.jpg
rename to astro-site/src/content/books/covers/a-liberated-mind-cover.jpg
diff --git a/src/content/books/covers/antifragile-cover.jpg b/astro-site/src/content/books/covers/antifragile-cover.jpg
similarity index 100%
rename from src/content/books/covers/antifragile-cover.jpg
rename to astro-site/src/content/books/covers/antifragile-cover.jpg
diff --git a/src/content/books/covers/brave-new-world-cover.jpeg b/astro-site/src/content/books/covers/brave-new-world-cover.jpeg
similarity index 100%
rename from src/content/books/covers/brave-new-world-cover.jpeg
rename to astro-site/src/content/books/covers/brave-new-world-cover.jpeg
diff --git a/src/content/books/covers/breaking-out-of-homeostasis-cover.jpg b/astro-site/src/content/books/covers/breaking-out-of-homeostasis-cover.jpg
similarity index 100%
rename from src/content/books/covers/breaking-out-of-homeostasis-cover.jpg
rename to astro-site/src/content/books/covers/breaking-out-of-homeostasis-cover.jpg
diff --git a/src/content/books/covers/capitalist-realism-cover.jpg b/astro-site/src/content/books/covers/capitalist-realism-cover.jpg
similarity index 100%
rename from src/content/books/covers/capitalist-realism-cover.jpg
rename to astro-site/src/content/books/covers/capitalist-realism-cover.jpg
diff --git a/src/content/books/covers/dark-money-cover.jpeg b/astro-site/src/content/books/covers/dark-money-cover.jpeg
similarity index 100%
rename from src/content/books/covers/dark-money-cover.jpeg
rename to astro-site/src/content/books/covers/dark-money-cover.jpeg
diff --git a/src/content/books/covers/debt-the-first-5000-years-cover.jpg b/astro-site/src/content/books/covers/debt-the-first-5000-years-cover.jpg
similarity index 100%
rename from src/content/books/covers/debt-the-first-5000-years-cover.jpg
rename to astro-site/src/content/books/covers/debt-the-first-5000-years-cover.jpg
diff --git a/src/content/books/covers/flow-cover.jpg b/astro-site/src/content/books/covers/flow-cover.jpg
similarity index 100%
rename from src/content/books/covers/flow-cover.jpg
rename to astro-site/src/content/books/covers/flow-cover.jpg
diff --git a/src/content/books/covers/fooled-by-randomness-cover.jpg b/astro-site/src/content/books/covers/fooled-by-randomness-cover.jpg
similarity index 100%
rename from src/content/books/covers/fooled-by-randomness-cover.jpg
rename to astro-site/src/content/books/covers/fooled-by-randomness-cover.jpg
diff --git a/src/content/books/covers/gateless-cover.jpg b/astro-site/src/content/books/covers/gateless-cover.jpg
similarity index 100%
rename from src/content/books/covers/gateless-cover.jpg
rename to astro-site/src/content/books/covers/gateless-cover.jpg
diff --git a/src/content/books/covers/i-wrote-this-book-because-i-love-you-cover.jpg b/astro-site/src/content/books/covers/i-wrote-this-book-because-i-love-you-cover.jpg
similarity index 100%
rename from src/content/books/covers/i-wrote-this-book-because-i-love-you-cover.jpg
rename to astro-site/src/content/books/covers/i-wrote-this-book-because-i-love-you-cover.jpg
diff --git a/src/content/books/covers/influencer-die-ideologie-des-werbekoerpers-cover.webp b/astro-site/src/content/books/covers/influencer-die-ideologie-des-werbekoerpers-cover.webp
similarity index 100%
rename from src/content/books/covers/influencer-die-ideologie-des-werbekoerpers-cover.webp
rename to astro-site/src/content/books/covers/influencer-die-ideologie-des-werbekoerpers-cover.webp
diff --git a/src/content/books/covers/kill-all-normies-cover.webp b/astro-site/src/content/books/covers/kill-all-normies-cover.webp
similarity index 100%
rename from src/content/books/covers/kill-all-normies-cover.webp
rename to astro-site/src/content/books/covers/kill-all-normies-cover.webp
diff --git a/src/content/books/covers/less-is-more-cover.webp b/astro-site/src/content/books/covers/less-is-more-cover.webp
similarity index 100%
rename from src/content/books/covers/less-is-more-cover.webp
rename to astro-site/src/content/books/covers/less-is-more-cover.webp
diff --git a/src/content/books/covers/mans-search-for-meaning-cover.jpg b/astro-site/src/content/books/covers/mans-search-for-meaning-cover.jpg
similarity index 100%
rename from src/content/books/covers/mans-search-for-meaning-cover.jpg
rename to astro-site/src/content/books/covers/mans-search-for-meaning-cover.jpg
diff --git a/src/content/books/covers/of-mice-and-men-cover.jpg b/astro-site/src/content/books/covers/of-mice-and-men-cover.jpg
similarity index 100%
rename from src/content/books/covers/of-mice-and-men-cover.jpg
rename to astro-site/src/content/books/covers/of-mice-and-men-cover.jpg
diff --git a/src/content/books/covers/seeing-that-frees-cover.jpg b/astro-site/src/content/books/covers/seeing-that-frees-cover.jpg
similarity index 100%
rename from src/content/books/covers/seeing-that-frees-cover.jpg
rename to astro-site/src/content/books/covers/seeing-that-frees-cover.jpg
diff --git a/src/content/books/covers/skin-in-the-game-cover.jpg b/astro-site/src/content/books/covers/skin-in-the-game-cover.jpg
similarity index 100%
rename from src/content/books/covers/skin-in-the-game-cover.jpg
rename to astro-site/src/content/books/covers/skin-in-the-game-cover.jpg
diff --git a/src/content/books/covers/survival-of-the-richest-cover.jpg b/astro-site/src/content/books/covers/survival-of-the-richest-cover.jpg
similarity index 100%
rename from src/content/books/covers/survival-of-the-richest-cover.jpg
rename to astro-site/src/content/books/covers/survival-of-the-richest-cover.jpg
diff --git a/src/content/books/covers/the-art-of-learning-cover.jpg b/astro-site/src/content/books/covers/the-art-of-learning-cover.jpg
similarity index 100%
rename from src/content/books/covers/the-art-of-learning-cover.jpg
rename to astro-site/src/content/books/covers/the-art-of-learning-cover.jpg
diff --git a/src/content/books/covers/the-black-swan-cover.webp b/astro-site/src/content/books/covers/the-black-swan-cover.webp
similarity index 100%
rename from src/content/books/covers/the-black-swan-cover.webp
rename to astro-site/src/content/books/covers/the-black-swan-cover.webp
diff --git a/src/content/books/covers/the-count-of-monte-cristo-cover.jpg b/astro-site/src/content/books/covers/the-count-of-monte-cristo-cover.jpg
similarity index 100%
rename from src/content/books/covers/the-count-of-monte-cristo-cover.jpg
rename to astro-site/src/content/books/covers/the-count-of-monte-cristo-cover.jpg
diff --git a/src/content/books/covers/the-divide-cover.jpg b/astro-site/src/content/books/covers/the-divide-cover.jpg
similarity index 100%
rename from src/content/books/covers/the-divide-cover.jpg
rename to astro-site/src/content/books/covers/the-divide-cover.jpg
diff --git a/src/content/books/covers/the-mind-illuminated-cover.jpg b/astro-site/src/content/books/covers/the-mind-illuminated-cover.jpg
similarity index 100%
rename from src/content/books/covers/the-mind-illuminated-cover.jpg
rename to astro-site/src/content/books/covers/the-mind-illuminated-cover.jpg
diff --git a/src/content/books/covers/the-paleo-manifesto-cover.jpg b/astro-site/src/content/books/covers/the-paleo-manifesto-cover.jpg
similarity index 100%
rename from src/content/books/covers/the-paleo-manifesto-cover.jpg
rename to astro-site/src/content/books/covers/the-paleo-manifesto-cover.jpg
diff --git a/src/content/books/covers/the-science-of-enlightenment-cover.jpg b/astro-site/src/content/books/covers/the-science-of-enlightenment-cover.jpg
similarity index 100%
rename from src/content/books/covers/the-science-of-enlightenment-cover.jpg
rename to astro-site/src/content/books/covers/the-science-of-enlightenment-cover.jpg
diff --git a/src/content/books/covers/vagabonding-cover.jpg b/astro-site/src/content/books/covers/vagabonding-cover.jpg
similarity index 100%
rename from src/content/books/covers/vagabonding-cover.jpg
rename to astro-site/src/content/books/covers/vagabonding-cover.jpg
diff --git a/src/content/books/covers/we-learn-nothing-cover.jpg b/astro-site/src/content/books/covers/we-learn-nothing-cover.jpg
similarity index 100%
rename from src/content/books/covers/we-learn-nothing-cover.jpg
rename to astro-site/src/content/books/covers/we-learn-nothing-cover.jpg
diff --git a/src/content/books/covers/winners-take-all-cover.jpg b/astro-site/src/content/books/covers/winners-take-all-cover.jpg
similarity index 100%
rename from src/content/books/covers/winners-take-all-cover.jpg
rename to astro-site/src/content/books/covers/winners-take-all-cover.jpg
diff --git a/src/content/books/dark-money.md b/astro-site/src/content/books/dark-money.md
similarity index 100%
rename from src/content/books/dark-money.md
rename to astro-site/src/content/books/dark-money.md
diff --git a/src/content/books/debt-the-first-5000-years.md b/astro-site/src/content/books/debt-the-first-5000-years.md
similarity index 100%
rename from src/content/books/debt-the-first-5000-years.md
rename to astro-site/src/content/books/debt-the-first-5000-years.md
diff --git a/src/content/books/flow.md b/astro-site/src/content/books/flow.md
similarity index 100%
rename from src/content/books/flow.md
rename to astro-site/src/content/books/flow.md
diff --git a/src/content/books/fooled-by-randomness.md b/astro-site/src/content/books/fooled-by-randomness.md
similarity index 100%
rename from src/content/books/fooled-by-randomness.md
rename to astro-site/src/content/books/fooled-by-randomness.md
diff --git a/src/content/books/gateless.md b/astro-site/src/content/books/gateless.md
similarity index 100%
rename from src/content/books/gateless.md
rename to astro-site/src/content/books/gateless.md
diff --git a/src/content/books/i-wrote-this-book-because-i-love-you.md b/astro-site/src/content/books/i-wrote-this-book-because-i-love-you.md
similarity index 100%
rename from src/content/books/i-wrote-this-book-because-i-love-you.md
rename to astro-site/src/content/books/i-wrote-this-book-because-i-love-you.md
diff --git a/src/content/books/influencer-die-ideologie-des-werbekoerpers.md b/astro-site/src/content/books/influencer-die-ideologie-des-werbekoerpers.md
similarity index 100%
rename from src/content/books/influencer-die-ideologie-des-werbekoerpers.md
rename to astro-site/src/content/books/influencer-die-ideologie-des-werbekoerpers.md
diff --git a/src/content/books/kill-all-normies.md b/astro-site/src/content/books/kill-all-normies.md
similarity index 100%
rename from src/content/books/kill-all-normies.md
rename to astro-site/src/content/books/kill-all-normies.md
diff --git a/src/content/books/less-is-more.md b/astro-site/src/content/books/less-is-more.md
similarity index 100%
rename from src/content/books/less-is-more.md
rename to astro-site/src/content/books/less-is-more.md
diff --git a/src/content/books/mans-search-for-meaning.md b/astro-site/src/content/books/mans-search-for-meaning.md
similarity index 100%
rename from src/content/books/mans-search-for-meaning.md
rename to astro-site/src/content/books/mans-search-for-meaning.md
diff --git a/src/content/books/of-mice-and-men.md b/astro-site/src/content/books/of-mice-and-men.md
similarity index 100%
rename from src/content/books/of-mice-and-men.md
rename to astro-site/src/content/books/of-mice-and-men.md
diff --git a/src/content/books/seeing-that-frees.md b/astro-site/src/content/books/seeing-that-frees.md
similarity index 100%
rename from src/content/books/seeing-that-frees.md
rename to astro-site/src/content/books/seeing-that-frees.md
diff --git a/src/content/books/skin-in-the-game.md b/astro-site/src/content/books/skin-in-the-game.md
similarity index 100%
rename from src/content/books/skin-in-the-game.md
rename to astro-site/src/content/books/skin-in-the-game.md
diff --git a/src/content/books/survival-of-the-richest.md b/astro-site/src/content/books/survival-of-the-richest.md
similarity index 100%
rename from src/content/books/survival-of-the-richest.md
rename to astro-site/src/content/books/survival-of-the-richest.md
diff --git a/src/content/books/the-art-of-learning.md b/astro-site/src/content/books/the-art-of-learning.md
similarity index 100%
rename from src/content/books/the-art-of-learning.md
rename to astro-site/src/content/books/the-art-of-learning.md
diff --git a/src/content/books/the-count-of-monte-cristo.md b/astro-site/src/content/books/the-count-of-monte-cristo.md
similarity index 100%
rename from src/content/books/the-count-of-monte-cristo.md
rename to astro-site/src/content/books/the-count-of-monte-cristo.md
diff --git a/src/content/books/the-divide.md b/astro-site/src/content/books/the-divide.md
similarity index 100%
rename from src/content/books/the-divide.md
rename to astro-site/src/content/books/the-divide.md
diff --git a/src/content/books/the-mind-illuminated.md b/astro-site/src/content/books/the-mind-illuminated.md
similarity index 100%
rename from src/content/books/the-mind-illuminated.md
rename to astro-site/src/content/books/the-mind-illuminated.md
diff --git a/src/content/books/the-paleo-manifesto.md b/astro-site/src/content/books/the-paleo-manifesto.md
similarity index 100%
rename from src/content/books/the-paleo-manifesto.md
rename to astro-site/src/content/books/the-paleo-manifesto.md
diff --git a/src/content/books/the-science-of-enlightenment.md b/astro-site/src/content/books/the-science-of-enlightenment.md
similarity index 100%
rename from src/content/books/the-science-of-enlightenment.md
rename to astro-site/src/content/books/the-science-of-enlightenment.md
diff --git a/src/content/books/vagabonding.md b/astro-site/src/content/books/vagabonding.md
similarity index 100%
rename from src/content/books/vagabonding.md
rename to astro-site/src/content/books/vagabonding.md
diff --git a/src/content/books/we-learn-nothing.md b/astro-site/src/content/books/we-learn-nothing.md
similarity index 100%
rename from src/content/books/we-learn-nothing.md
rename to astro-site/src/content/books/we-learn-nothing.md
diff --git a/src/content/books/winner-takes-all.md b/astro-site/src/content/books/winner-takes-all.md
similarity index 100%
rename from src/content/books/winner-takes-all.md
rename to astro-site/src/content/books/winner-takes-all.md
diff --git a/src/content/config.ts b/astro-site/src/content/config.ts
similarity index 100%
rename from src/content/config.ts
rename to astro-site/src/content/config.ts
diff --git a/src/content/films/palm-springs.md b/astro-site/src/content/films/palm-springs.md
similarity index 100%
rename from src/content/films/palm-springs.md
rename to astro-site/src/content/films/palm-springs.md
diff --git a/src/content/musicians/ski-aggu.md b/astro-site/src/content/musicians/ski-aggu.md
similarity index 100%
rename from src/content/musicians/ski-aggu.md
rename to astro-site/src/content/musicians/ski-aggu.md
diff --git a/src/content/pages/colophon.mdx b/astro-site/src/content/pages/colophon.mdx
similarity index 100%
rename from src/content/pages/colophon.mdx
rename to astro-site/src/content/pages/colophon.mdx
diff --git a/src/content/pages/imprint.md b/astro-site/src/content/pages/imprint.md
similarity index 100%
rename from src/content/pages/imprint.md
rename to astro-site/src/content/pages/imprint.md
diff --git a/src/content/pages/privacy-policy.md b/astro-site/src/content/pages/privacy-policy.md
similarity index 100%
rename from src/content/pages/privacy-policy.md
rename to astro-site/src/content/pages/privacy-policy.md
diff --git a/src/content/pages/uses.md b/astro-site/src/content/pages/uses.md
similarity index 100%
rename from src/content/pages/uses.md
rename to astro-site/src/content/pages/uses.md
diff --git a/src/content/projects/hasanhub-com.mdx b/astro-site/src/content/projects/hasanhub-com.mdx
similarity index 100%
rename from src/content/projects/hasanhub-com.mdx
rename to astro-site/src/content/projects/hasanhub-com.mdx
diff --git a/src/content/projects/hausgemacht.mdx b/astro-site/src/content/projects/hausgemacht.mdx
similarity index 100%
rename from src/content/projects/hausgemacht.mdx
rename to astro-site/src/content/projects/hausgemacht.mdx
diff --git a/src/content/projects/images/hasanhub-plausible-april-may-2022.png b/astro-site/src/content/projects/images/hasanhub-plausible-april-may-2022.png
similarity index 100%
rename from src/content/projects/images/hasanhub-plausible-april-may-2022.png
rename to astro-site/src/content/projects/images/hasanhub-plausible-april-may-2022.png
diff --git a/src/content/projects/images/hasanhub-plausible-august-2022.png b/astro-site/src/content/projects/images/hasanhub-plausible-august-2022.png
similarity index 100%
rename from src/content/projects/images/hasanhub-plausible-august-2022.png
rename to astro-site/src/content/projects/images/hasanhub-plausible-august-2022.png
diff --git a/src/content/projects/images/hasanhub-plausible-december-2022.png b/astro-site/src/content/projects/images/hasanhub-plausible-december-2022.png
similarity index 100%
rename from src/content/projects/images/hasanhub-plausible-december-2022.png
rename to astro-site/src/content/projects/images/hasanhub-plausible-december-2022.png
diff --git a/src/content/projects/images/hasanhub-screenshot.png b/astro-site/src/content/projects/images/hasanhub-screenshot.png
similarity index 100%
rename from src/content/projects/images/hasanhub-screenshot.png
rename to astro-site/src/content/projects/images/hasanhub-screenshot.png
diff --git a/src/content/projects/images/hausgemacht-screenshot.png b/astro-site/src/content/projects/images/hausgemacht-screenshot.png
similarity index 100%
rename from src/content/projects/images/hausgemacht-screenshot.png
rename to astro-site/src/content/projects/images/hausgemacht-screenshot.png
diff --git a/src/content/projects/images/hausgemacht-vibechecker-screenshots.png b/astro-site/src/content/projects/images/hausgemacht-vibechecker-screenshots.png
similarity index 100%
rename from src/content/projects/images/hausgemacht-vibechecker-screenshots.png
rename to astro-site/src/content/projects/images/hausgemacht-vibechecker-screenshots.png
diff --git a/src/content/projects/images/mitentscheiden-kabine-clip.gif b/astro-site/src/content/projects/images/mitentscheiden-kabine-clip.gif
similarity index 100%
rename from src/content/projects/images/mitentscheiden-kabine-clip.gif
rename to astro-site/src/content/projects/images/mitentscheiden-kabine-clip.gif
diff --git a/src/content/projects/images/mitentscheiden-kabine-screenshot.png b/astro-site/src/content/projects/images/mitentscheiden-kabine-screenshot.png
similarity index 100%
rename from src/content/projects/images/mitentscheiden-kabine-screenshot.png
rename to astro-site/src/content/projects/images/mitentscheiden-kabine-screenshot.png
diff --git a/src/content/projects/images/mitentscheiden-results.png b/astro-site/src/content/projects/images/mitentscheiden-results.png
similarity index 100%
rename from src/content/projects/images/mitentscheiden-results.png
rename to astro-site/src/content/projects/images/mitentscheiden-results.png
diff --git a/src/content/projects/images/mitentscheiden-screenshot.png b/astro-site/src/content/projects/images/mitentscheiden-screenshot.png
similarity index 100%
rename from src/content/projects/images/mitentscheiden-screenshot.png
rename to astro-site/src/content/projects/images/mitentscheiden-screenshot.png
diff --git a/src/content/projects/images/mitentscheiden-screenshots.jpg b/astro-site/src/content/projects/images/mitentscheiden-screenshots.jpg
similarity index 100%
rename from src/content/projects/images/mitentscheiden-screenshots.jpg
rename to astro-site/src/content/projects/images/mitentscheiden-screenshots.jpg
diff --git a/src/content/projects/images/mitentscheiden-shareables.jpg b/astro-site/src/content/projects/images/mitentscheiden-shareables.jpg
similarity index 100%
rename from src/content/projects/images/mitentscheiden-shareables.jpg
rename to astro-site/src/content/projects/images/mitentscheiden-shareables.jpg
diff --git a/src/content/projects/images/preismonitor-plausible-may-july.png b/astro-site/src/content/projects/images/preismonitor-plausible-may-july.png
similarity index 100%
rename from src/content/projects/images/preismonitor-plausible-may-july.png
rename to astro-site/src/content/projects/images/preismonitor-plausible-may-july.png
diff --git a/src/content/projects/images/preismonitor-screenshot.png b/astro-site/src/content/projects/images/preismonitor-screenshot.png
similarity index 100%
rename from src/content/projects/images/preismonitor-screenshot.png
rename to astro-site/src/content/projects/images/preismonitor-screenshot.png
diff --git a/src/content/projects/mitentscheiden-at.mdx b/astro-site/src/content/projects/mitentscheiden-at.mdx
similarity index 100%
rename from src/content/projects/mitentscheiden-at.mdx
rename to astro-site/src/content/projects/mitentscheiden-at.mdx
diff --git a/src/content/projects/preismonitor-at.mdx b/astro-site/src/content/projects/preismonitor-at.mdx
similarity index 100%
rename from src/content/projects/preismonitor-at.mdx
rename to astro-site/src/content/projects/preismonitor-at.mdx
diff --git a/src/content/quotes/write-in-blood.md b/astro-site/src/content/quotes/write-in-blood.md
similarity index 100%
rename from src/content/quotes/write-in-blood.md
rename to astro-site/src/content/quotes/write-in-blood.md
diff --git a/src/content/shows/bojack-horseman.md b/astro-site/src/content/shows/bojack-horseman.md
similarity index 100%
rename from src/content/shows/bojack-horseman.md
rename to astro-site/src/content/shows/bojack-horseman.md
diff --git a/src/data/socials.ts b/astro-site/src/data/socials.ts
similarity index 100%
rename from src/data/socials.ts
rename to astro-site/src/data/socials.ts
diff --git a/src/env.d.ts b/astro-site/src/env.d.ts
similarity index 100%
rename from src/env.d.ts
rename to astro-site/src/env.d.ts
diff --git a/src/images/chrcit-favicon.png b/astro-site/src/images/chrcit-favicon.png
similarity index 100%
rename from src/images/chrcit-favicon.png
rename to astro-site/src/images/chrcit-favicon.png
diff --git a/src/images/cut-out.png b/astro-site/src/images/cut-out.png
similarity index 100%
rename from src/images/cut-out.png
rename to astro-site/src/images/cut-out.png
diff --git a/src/images/home-profile-camera.jpg b/astro-site/src/images/home-profile-camera.jpg
similarity index 100%
rename from src/images/home-profile-camera.jpg
rename to astro-site/src/images/home-profile-camera.jpg
diff --git a/src/images/home-profile.jpg b/astro-site/src/images/home-profile.jpg
similarity index 100%
rename from src/images/home-profile.jpg
rename to astro-site/src/images/home-profile.jpg
diff --git a/src/images/profile-frontal.jpg b/astro-site/src/images/profile-frontal.jpg
similarity index 100%
rename from src/images/profile-frontal.jpg
rename to astro-site/src/images/profile-frontal.jpg
diff --git a/src/layouts/ArticleLayout.astro b/astro-site/src/layouts/ArticleLayout.astro
similarity index 100%
rename from src/layouts/ArticleLayout.astro
rename to astro-site/src/layouts/ArticleLayout.astro
diff --git a/src/layouts/BaseLayout.astro b/astro-site/src/layouts/BaseLayout.astro
similarity index 100%
rename from src/layouts/BaseLayout.astro
rename to astro-site/src/layouts/BaseLayout.astro
diff --git a/src/layouts/ListLayout.astro b/astro-site/src/layouts/ListLayout.astro
similarity index 100%
rename from src/layouts/ListLayout.astro
rename to astro-site/src/layouts/ListLayout.astro
diff --git a/src/layouts/RootLayout.astro b/astro-site/src/layouts/RootLayout.astro
similarity index 100%
rename from src/layouts/RootLayout.astro
rename to astro-site/src/layouts/RootLayout.astro
diff --git a/src/pages/[slug].astro b/astro-site/src/pages/[slug].astro
similarity index 100%
rename from src/pages/[slug].astro
rename to astro-site/src/pages/[slug].astro
diff --git a/src/pages/articles.astro b/astro-site/src/pages/articles.astro
similarity index 100%
rename from src/pages/articles.astro
rename to astro-site/src/pages/articles.astro
diff --git a/src/pages/articles/[slug].astro b/astro-site/src/pages/articles/[slug].astro
similarity index 100%
rename from src/pages/articles/[slug].astro
rename to astro-site/src/pages/articles/[slug].astro
diff --git a/src/pages/books.astro b/astro-site/src/pages/books.astro
similarity index 100%
rename from src/pages/books.astro
rename to astro-site/src/pages/books.astro
diff --git a/src/pages/books/[slug].astro b/astro-site/src/pages/books/[slug].astro
similarity index 100%
rename from src/pages/books/[slug].astro
rename to astro-site/src/pages/books/[slug].astro
diff --git a/src/pages/index.astro b/astro-site/src/pages/index.astro
similarity index 100%
rename from src/pages/index.astro
rename to astro-site/src/pages/index.astro
diff --git a/src/pages/projects.astro b/astro-site/src/pages/projects.astro
similarity index 100%
rename from src/pages/projects.astro
rename to astro-site/src/pages/projects.astro
diff --git a/src/pages/projects/[slug].astro b/astro-site/src/pages/projects/[slug].astro
similarity index 100%
rename from src/pages/projects/[slug].astro
rename to astro-site/src/pages/projects/[slug].astro
diff --git a/tailwind.config.mjs b/astro-site/tailwind.config.mjs
similarity index 100%
rename from tailwind.config.mjs
rename to astro-site/tailwind.config.mjs
diff --git a/content/articles/2023-year-in-review.mdx b/content/articles/2023-year-in-review.mdx
new file mode 100644
index 0000000..1487e49
--- /dev/null
+++ b/content/articles/2023-year-in-review.mdx
@@ -0,0 +1,283 @@
+---
+title: 2023 year in review
+description: My 2023 year in review.
+tags: []
+image: ./images/der-wanderer-ueber-dem-nebelmeer.jpg
+publishedAt: 2023-12-20
+---
+
+import { Tweet } from "@astro-community/astro-embed-twitter";
+import AndererseitsInstagram from "../../components/embeds/AndererseitsInstagram.astro"
+import ExternalLink from "../../components/ExternalLink.astro"
+import { YouTube } from "@astro-community/astro-embed-youtube";
+
+2023 was a wild ride.
+
+I started the year with a new job, got fired after 3 weeks, got involved in politics, made apps for sex positive parties, started another new job, was invited to consult an Austrian minister on a new law and much more.
+
+## I launched my first project to 30.000 people
+The political streamer Hasanabi allows his fans to clip his stream VODs, upload those clips to YouTube and make money from his content.
+
+Due to this, there are 100+ different YouTube channels dedicated to him. Some of these fan channels have over 100.000 subscribers. I saw the opportunity to build something useful for his community by creating a web app which aggregates all of these channels into one feed. It's called hasanhub.com .
+
+
+
+I first launched the project in April 2022 and Hasan looked at it on his stream while live to roughly 30.000 people.
+
+
+
+The active users stagnated at around 30 per day until in December 2022 and January 2023 Hasan opened it on stream again. After that exposure, the active users went up to 300-400 per day.
+
+
+
+In January 2023 I decided to create a separate Twitter account (@hasanhub_com ) for the project and had a tweet go viral with ~500.000 impressions:
+
+
+
+Check out the [project page for Hasanhub](/projects/hasanhub-com).
+
+## I got my dream job (and was fired after 3 weeks)
+Between July and September 2022 I spent ~50 hours researching and ideating on a link-in-bio style microsite builder. The concept was that instead of just links to external sites, the app would automatically fetch the latest content from Twitter, Instagram, YouTube, Spotify, RSS, etc.
+
+I submitted the idea to an incubator but it was rejected.
+
+In January 2023 I talked about it on Twitter and got a reply from a company called Bento. They were building just that and were looking for a founding product engineer:
+
+
+After a few rounds of interviews and a 1 day hackathon , I received an offer and accepted it.
+
+For my first week I traveled to Berlin to meet the team:
+
+
+
+My first feature was a gorgeous settings UI + some auth related stuff. I shipped it after 3 days:
+
+
+Besides the regular marketing by Bento I decided to also post about it on multiple subreddits. The posts got ~300.000 impressions, thousands of upvotes and hundreds of comments. It also drove a good bit of extra sign-ups to Bento.
+
+
+
+ I started a new job this week and shipped this gorgeous settings UI
+ yesterday
+
+ byu/chrcit inwebdev
+
+
+After my second week I shipped new widgets for Figma, Dribbble and Behance:
+
+
+In my third week I worked on interactive media player widgets for Spotify with beautiful animations:
+
+
+But before I could ship those I got fired.
+
+Earlier in the same week I asked to get a few days more time to work on the next feature. I had been working 50–60 hours per week up to that point and wanted to take the weekend off to recharge.
+
+I told my CTO that developing and launching a big, new feature every week wouldn't be sustainable for me long-term. He told me that that is startup life and that we could talk more about it on Friday with the CEO. On Friday I got fired for not being a good fit.
+
+10 weeks later Bento was sold to Linktree only 6 months after it was first launched. This is good for Bento and amazing for Linktree.
+
+I don't hold any ill will towards the people at Bento. In the few weeks I worked there I learned a lot about product development, engineering, design, and marketing from the team. I also figured out what I want and don't want from a job.
+
+## An app I made helped get a socialist elected
+Georg , a good friend of mine, is active in the SPÖ (the social democratic party of Austria). In the early summer of 2023 they were holding an internal election for the new party leader.
+
+The Junge Generation ("young generation" in German), the youth wing of the party wanted to create an app to help people decide which of the internal candidates to vote for.
+
+Georg asked me if I wanted to collaborate on this application and I agreed. The concept was modelled after wahlkabine.at , a political questionnaire which helps you find the party which best represents your political views.
+
+We went from idea to launch of mitentscheiden.at in 3 weeks.
+
+
+
+The 3 candidates all answered 42 questions via the app and the users could then answer the same questions. After finishing the questionnaire the user is shown how much they match with the 3 candidates as well as multiple pages to compare their answers with the candidates.
+
+
+
+
+
+The launch was a big success with around ~27.000 people filling out the questionnaire. The app also went viral on Twitter with over 400.000 impressions and multiple high profile people in the Austrian political bubble sharing it.
+
+It was covered by [multiple](https://www.puls24.at/news/politik/spoe-wahl-41-fragen-und-die-antworten-von-babler-doskozil-und-rendi-wagner/295388) [national](https://kurier.at/politik/inland/test-fuer-nicht-mitglieder-welcher-spoe-kandidat-wuerde-zu-ihnen-passen/402422507) [newspapers](https://www.derstandard.at/story/2000145782957/was-man-ueber-die-rote-vorsitzwahl-wissen-muss), [meme](https://www.instagram.com/p/CrX6u3nMem0/) [pages](https://www.instagram.com/p/CrYPyLWsp7w) and the largest TV news program of the country (ZIB 1).
+
+A question regarding removing abortion from the criminal code resulted in a controversy. The more conservative candidate (and front-runner) Doskozil at first answered with “no” but after a backlash on social media changed his answer to “yes”.
+
+The incumbent candidate Rendi-Wagner dropped out of the race after she came in last in a party wide poll.
+
+The election was held in June and ~600 party officials voted. Initially it was announced that the more conservative candidate Doskozil won with 52% of the vote.
+
+But a few days later it was discovered that there was a [mistake in the Excel sheet](https://www.theguardian.com/world/2023/jun/05/austrian-social-democrats-announce-wrong-leader-after-technical-error) used to calculate the results. The votes were flipped by accident and actually the more radical candidate Babler won.
+
+Due to the close result it's possible that the controversy around the abortion question could have swayed the election in Babler's favor.
+
+2 months after the launch I used the project as a case study for my first dev talk which I held at the Technical University of Vienna. I uploaded the recording to YouTube and got over 1000 views on it in a few days.
+
+
+
+I also joined the party and founded the Web Task Force together with [Georg Windhaber](https://georgwindhaber.com).
+
+We are currently working on multiple web projects to campaign for the upcoming national election in September 2024 as well as the European Union elections in July.
+
+
+
+Check out the [project page for mitentscheiden.at](/projects/mitentscheiden-at).
+
+## A (shitty) website I made might change the law
+In 2022 and 2023 inflation was rising in Austria and all over the world. A big driver was the increase in grocery prices.
+
+In May 2023 Martin Kocher, the Austrian Minister for Labour and Economy announced plans for a state grocery price tracker for a few selected products. The plan was quite vague and it wouldn't launch anytime before fall 2023.
+
+Lukas , a good friend of mine, suggested that we should just build this ourselves.
+
+We went from idea to launch in 2 weeks. The site is called preismonitor.at and it's a simple website which shows the price development of ~300 products in 3 supermarkets.
+
+
+
+Even though we launched as fast as possible there were 3 other price trackers that were released before our site:
+
+- heisse-preisse.io by Mario Zechner
+- teuerungsportal.at by Bernhard Ruckenstuhl
+- preisrunter.at by David Wurm
+
+I reached out to all of them and we started a group chat.
+
+
+
+Both Lukas and I didn't have much time to work on the project after launch. Due to the other projects existing we didn't see a strong need to continue developing it further for the moment.
+
+We still got a few mentions in both national and international media.
+
+
+
+In July the Austrian Federal Competition Authority reached out to us and the other grocery price providers to ask us a few questions about our projects. They have been investigating the supermarket industry since fall 2022.
+
+Our answers were then used in a final report which was handed over to Martin Kocher, the Minister of Labour and Economy.
+
+The report recommended implementing a law which would force supermarkets to report their price data to the ministry which then would make it available via an API.
+
+This would enable developers, researchers and journalists to utilize the data for many varied use cases.
+
+
+
+In September the Minister for Labour and Economy invited us to consult him on the exact implementation of the law. We were invited together with our friends from heisse-preisse.io , teuerungsportal.at and preisrunter.at .
+
+
+
+In mid October our crawler stopped working and both Lukas and I were too busy to fix it. We decided to pause development until we have more time.
+
+We intend to dump our crawler and instead use the data from heisse-preisse.io or the government API (if it actually comes).
+
+A long-term idea is to create a grocery list app which is enhanced by this pricing data. This would allow users to see which supermarket is the cheapest for their current grocery list.
+
+Check out the [project page for Preismonitor](/projects/preismonitor-at).
+
+## I make apps for sex positive parties
+hausgemacht ("homemade" in German) is the biggest techno collective in Vienna. They organize events in various clubs in the city. Besides regular techno clubbings they also host sex positive parties.
+
+The people behind it are focused on creating a safeR space for their guests, especially FLINTA* people. They make this possible via an online application process before the sex positive parties and a large awareness team looking after the safety and well-being of the guests during the event.
+
+After attending my first event in February 2023 I decided I wanted to join the collective. I reached out to them and offered to support them with their custom-made application process.
+
+
+
+After meeting with the internet team and getting to know each other they invited me to become a member of the collective.
+
+The application process is handled via a web app built by members of the collective. Because of the sensitive nature of the data the app is built with a strong focus on data protection and privacy.
+
+Alois Paulin built the backend which is architected to minimize data stored and disclosed to members of the organisation. There is also great care to ensure that all personal data is encrypted and stored securely. Once the need for the data is gone it is deleted.
+
+Patrick Ludewig designed and built the frontend. Vlad I. acted as a product owner/manager for this and many other projects.
+
+This year I focused on improving and extending the way we check in our guests.
+
+The team built a web app which allows guests to anonymously check in via a passport scanner. The app connects to the scanner and matches the name against the ticketing system. The person selecting the guest will only see the image read from the passport but will not see the name.
+
+The data from the passport (i.e. the image) is only processed and transported in a secure local network. It never leaves the local network and is deleted immediately after the event.
+
+This approach is quite innovative and Alois even wrote a paper about it.
+
+
+
+Together with Alois I refactored the app to stabilize the integration of the passport scanner and simplify the UI for the end user. We also trialed processing other IDs via OCR.
+
+A few hours before an event in September our backend server went down and we didn't have time to port it to another infrastructure.
+
+I quickly built a new web app for scanning the ticket QR code and enriching it with our guest data. Our team was then able to use the app to check in or reject guests at the door.
+
+
+
+I kept iterating on this app for 2 other events and added many new features. The app is now used at all our events and is the main tool for checking in guests.
+
+
+
+The plan is to eventually merge the passport scanner and the ticket scanner into one application so both methods can be used interchangeably.
+
+Our focus for 2024 is to unify all the different systems into one web application. Over the next 2 years this will evolve into a bespoke event management system for the collective.
+
+Check out the [project page for hausgemacht](/projects/hausgemacht).
+
+## Scaling a media site to millions of users
+After getting fired I applied to around 15 companies and got a few offers.
+
+I accepted an offer from RegionalMedien Austria. They're a media company which publishes 200+ regional print newspapers in Austria as well as an online portal: meinbezirk.at .
+
+The current online platform is managed by an external provider. In 2023 the company started working on an in-house developed relaunch of the whole site.
+
+Currently the site gets around 1.000.000 unique daily visitors. The goal is to transition route by route from the old platform to the new one.
+
+In my first month I got promoted to Lead Full-Stack Developer and started taking ownership over the development process.
+
+I'm very excited to work on a project of this scale.
+
+## Starting an agency?
+I've been freelancing since early high school (2012) and after getting fired I briefly thought about doing it full-time.
+
+To get something out there I built a coming soon page in a few hours while hungover and launched it on Twitter:
+
+
+
+After a few weeks the idea subsided again but I wanted to keep freelancing on the side to fund some other projects of mine.
+
+I started working with a digital agency called Anwert where I do web consulting, build WordPress plugins and create some social media posts .
+
+Over the summer I started supporting Mindnode , one of the most popular Mac/iOS productivity apps, with their website.
+
+My biggest freelance project in 2023 was the relaunch of houseofstrauss.at and zoegernitz.com .
+
+Both sites were commissioned by a real estate company which bought and renovated the Casino Zögernitz which used to be the old concert halls of Johann Strauss and his son. The building now houses a museum, a restaurant, and a concert hall/event location.
+
+The project's creative director Georg Brennwald designed 20+ components for the site.
+I then implemented them as full-stack components in Next.js which can be composed together via a headless page builder inside of Sanity .
+
+I'm still not 100% certain where to take Arthouse in the future, but for, now I'm happy to have it as a vehicle for my freelancing work.
+
+If you are interested in collaborating on a project with me you can reach out via email . I offer an initial free 30 minutes consulting call and after that my rate is 100€ per hour.
+
+
+## Supporting journalism for and from people with disabilities
+andererseits ("otherwise" in German) is a media startup based in Vienna about, for and from people with disabilities. Markus , a good friend of mine told me about them early 2023 and suggested that I could support them with their IT systems.
+
+I was interested because I have a disability myself due to an accident 6 years ago and always wanted to work on a project which helps people with disabilities.
+
+
+
+In fall and winter 2023 I met up with Lukas , one of the co-founders of andererseits and I automated some of their internal processes. These automations have saved them around 10% of their person hours per week which allows them to focus more on their core business.
+
+At the end of the year I also wrote a newsletter about my experience with disability:
+
+
+
+
+## Prospects for 2024
+I could have never imagined how 2023 would turn out. The year brought tons of new experiences and many shifts in my perspective on life and success.
+
+In 2024 I'm going to continue working on many of the projects which started in 2023. For the first time in my life I'm not unsure about what I want to spend my time on.
+
+The only new thing planned in 2024 is creating YouTube videos about my projects and the societal context around them.
+
+---
+Thank you for reading this far. If you want to stay up to date with my projects you can follow me on Twitter , YouTube or Instagram .
\ No newline at end of file
diff --git a/content/articles/hello-world.mdx b/content/articles/hello-world.mdx
new file mode 100644
index 0000000..bac21e2
--- /dev/null
+++ b/content/articles/hello-world.mdx
@@ -0,0 +1,10 @@
+---
+title: Hello World
+description: My first article in the new Remix site
+date: 2024-01-01
+tags: [remix, react]
+---
+
+# Hello World
+
+This is my first article in the new Remix site. I'm excited to share my thoughts and experiences with you.
\ No newline at end of file
diff --git a/content/articles/images/der-wanderer-ueber-dem-nebelmeer.jpg b/content/articles/images/der-wanderer-ueber-dem-nebelmeer.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..b07c4717167719fb921d1e3359ee8959f3df8c65
GIT binary patch
literal 434154
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 $?7rifGBV1N0%aK^hdDt09
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