migrate from astro+netlify to tanstack start + self-hosted
Build & Deploy unom website / build (push) Successful in 12s
Build & Deploy unom website / deploy (push) Successful in 4s

Replace the Astro static site with a TanStack Start (Bun runtime) app and
add Dockerfile + compose files so the site can be served from home-main-2
behind the home-reverse-proxy-1 Caddy instead of Netlify. CI workflow
rewritten to build a container image and SSH-deploy to the home host.
This commit is contained in:
2026-05-26 10:57:16 +02:00
parent b203d1b58a
commit ce63faa8f3
42 changed files with 945 additions and 1043 deletions
+16
View File
@@ -0,0 +1,16 @@
node_modules
.output
.nitro
.tanstack
dist
.git
.gitea
.github
.idea
.vscode
.DS_Store
.env
.env.*
*.log
bun-debug.log*
README.md
+69 -37
View File
@@ -1,46 +1,78 @@
name: Deploy to Netlify
name: Build & Deploy unom website
run-name: ${{ gitea.actor }} is deploying unom/website
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize]
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4.2.2
- name: Repository Checkout
uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-${{ matrix.bun }}-bun-${{ hashFiles('**/bun.lockb') }}
restore-keys: |
${{ runner.os }}-${{ matrix.bun }}-bun-
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Set up Docker Buildx
env:
UNOM_PKGS_TOKEN: ${{ secrets.UNOM_PKGS_TOKEN }}
BUILDER: builder-unom-website
run: |
cat > /tmp/buildkitd.toml <<'EOF'
[registry."docker.io"]
mirrors = ["192.168.1.52:5000"]
[registry."192.168.1.52:5000"]
http = true
insecure = true
EOF
docker buildx rm "$BUILDER" 2>/dev/null || true
docker buildx create --name "$BUILDER" --use --bootstrap \
--driver docker-container \
--config /tmp/buildkitd.toml
- name: Build
run: bun run build
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v2
with:
publish-dir: './dist'
production-branch: main
deploy-message: "Deploy from Gitea Actions"
- name: Log in to Gitea registry
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 10
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
printf '%s' "$REGISTRY_TOKEN" | docker login git.unom.io -u "$REGISTRY_USER" --password-stdin
- name: Build & push
env:
BUILDER: builder-unom-website
IMAGE: git.unom.io/${{ gitea.repository }}
SHA: ${{ gitea.sha }}
run: |
docker buildx build \
--builder "$BUILDER" \
--push \
--file ./Dockerfile \
--tag "$IMAGE:latest" \
--tag "$IMAGE:$SHA" \
--cache-from "type=registry,ref=$IMAGE:cache" \
--cache-to "type=registry,ref=$IMAGE:cache,mode=min" \
.
- name: Tear down builder
if: always()
env:
BUILDER: builder-unom-website
run: |
docker buildx rm "$BUILDER" 2>/dev/null || true
deploy:
runs-on: ubuntu-24.04
needs: build
steps:
- name: Pull and start web
uses: appleboy/ssh-action@v1.2.5
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
port: ${{ secrets.DEPLOY_PORT }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
docker login git.unom.io -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_TOKEN }}
cd ~/unom-website
git fetch origin main
git reset --hard origin/main
docker compose -f compose.production.yml pull web
docker compose -f compose.production.yml up -d --no-build web
+10 -3
View File
@@ -1,7 +1,11 @@
# build output
dist/
# generated types
.astro/
.output/
.nitro/
.tanstack/
# generated router types
src/routeTree.gen.ts
# dependencies
node_modules/
@@ -11,7 +15,7 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
bun-debug.log*
# environment variables
.env
@@ -22,3 +26,6 @@ pnpm-debug.log*
# jetbrains setting folder
.idea/
# editor
.vscode/
+29
View File
@@ -0,0 +1,29 @@
FROM oven/bun:alpine AS base
RUN apk update && apk add --no-cache libc6-compat
## INSTALLER - install deps and build
FROM base AS installer
WORKDIR /app
COPY package.json bun.lock ./
RUN --mount=type=cache,target=/root/.bun/install/cache,sharing=shared \
bun install --frozen-lockfile
COPY . .
RUN bun run build
## RUNNER - minimal production image
FROM base AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 bunjs && \
adduser --system --uid 1001 web
COPY --from=installer --chown=web:bunjs /app/.output ./.output
USER web
CMD ["bun", ".output/server/index.mjs"]
+30 -7
View File
@@ -1,13 +1,36 @@
# Astro with Tailwind
# @unom/website
The unom.io marketing site. TanStack Start + Bun, deployed to home-main-2.
## Development
```sh
bun create astro@latest -- --template with-tailwindcss
bun install
bun run dev
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/with-tailwindcss)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/with-tailwindcss)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/with-tailwindcss/devcontainer.json)
Visit <http://localhost:3000>.
Astro comes with [Tailwind](https://tailwindcss.com) support out of the box. This example showcases how to style your Astro project with Tailwind.
## Production
For complete setup instructions, please see our [Tailwind Integration Guide](https://docs.astro.build/en/guides/integrations-guide/tailwind).
The repo is built into a container image (`git.unom.io/unom/website`) by Gitea
Actions on push to `main`, then deployed via SSH to `home-main-2`. The
container listens on port 3000 inside the network and is exposed on host port
3200, which Caddy on `home-reverse-proxy-1` reverse-proxies for unom.io and
www.unom.io.
Run the production image locally:
```sh
docker compose -f compose.production.yml pull
docker compose -f compose.production.yml up -d
```
## Required CI secrets
Set on the `unom/website` repo in Gitea Actions:
| Secret | Purpose |
| ------ | ------- |
| `REGISTRY_USER` / `REGISTRY_TOKEN` | Push to `git.unom.io` container registry |
| `DEPLOY_HOST` / `DEPLOY_USER` / `DEPLOY_PORT` / `DEPLOY_SSH_KEY` | SSH target on home-main-2 (private key matching the `unom-ci-deploy` authorized key) |
-15
View File
@@ -1,15 +0,0 @@
// @ts-check
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import mdx from '@astrojs/mdx';
// https://astro.build/config
export default defineConfig({
site: "https://www.vspace.one",
vite: {
plugins: [tailwindcss()]
},
integrations: [
mdx(),
],
});
+44
View File
@@ -0,0 +1,44 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"includes": [
"**/src/**/*",
"**/vite.config.ts",
"!**/src/routeTree.gen.ts",
"!**/src/styles/**/*.css"
]
},
"css": {
"parser": {
"tailwindDirectives": true
}
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
}
}
+247 -554
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
name: unom-website-prod
services:
web:
image: git.unom.io/unom/website:latest
restart: unless-stopped
ports:
- "3200:3000"
+9
View File
@@ -0,0 +1,9 @@
name: unom-website-dev
services:
web:
restart: unless-stopped
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
+32 -10
View File
@@ -1,20 +1,42 @@
{
"name": "@unom/website",
"private": true,
"type": "module",
"version": "0.0.1",
"imports": {
"#/*": "./src/*"
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
"dev": "vite dev --port 3000 --host",
"build": "vite build",
"start": "bun .output/server/index.mjs",
"typecheck": "tsc --noEmit",
"format": "biome format",
"lint": "biome lint",
"check": "biome check"
},
"dependencies": {
"@astrojs/mdx": "^4.3.0",
"@fontsource/inter": "^5.2.5",
"@tailwindcss/vite": "^4.1.3",
"astro": "^5.8.0",
"@tailwindcss/vite": "^4.3.0",
"@tanstack/react-router": "latest",
"@tanstack/react-start": "latest",
"@tanstack/router-plugin": "^1.168.9",
"class-variance-authority": "^0.7.1",
"tailwind-merge": "^3.3.0",
"tailwindcss": "^4.1.3"
"clsx": "^2.1.1",
"nitro": "^3.0.260429-beta",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-markdown": "^9.0.3",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.0"
},
"devDependencies": {
"@biomejs/biome": "2.4.15",
"@types/node": "^25.9.1",
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"typescript": "^6.0.3",
"vite": "^8.0.14",
"vite-tsconfig-paths": "^6.1.1"
}
}

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

+68
View File
@@ -0,0 +1,68 @@
import Section from "./Section";
type NavigationItem =
| { type: "link"; path: string; label: string }
| { type: "text"; content: string };
type NavigationGroup = {
title: string;
className?: string;
items: Array<NavigationItem>;
};
const tree: Array<NavigationGroup> = [
{
title: "Übersicht",
items: [{ type: "link", path: "/", label: "Startseite" }],
},
{
title: "Rechtliches",
items: [
{ type: "link", path: "/legal/imprint", label: "Impressum" },
{ type: "link", path: "/legal/privacy", label: "Datenschutzerklärung" },
],
},
{
title: "Zugehöriges",
items: [
{
type: "link",
path: "https://enrico.buehler.earth",
label: "Enrico Bühler",
},
],
},
{
title: "",
className: "ml-auto mr-0 self-end",
items: [{ type: "text", content: "Made with ❤️ in Rottweil" }],
},
];
export default function Footer() {
return (
<footer className="bg-neutral-accent">
<Section>
<div className="flex flex-row flex-wrap gap-12 w-full pb-8">
{tree.map((group) => (
<div key={group.title || "misc"} className={group.className}>
{group.title && <h3 className="mb-2">{group.title}</h3>}
<div className="flex flex-col">
{group.items.map((item, i) => {
if (item.type === "link") {
return (
<a key={`${item.path}-${i}`} href={item.path}>
{item.label}
</a>
);
}
return <p key={`text-${i}`}>{item.content}</p>;
})}
</div>
</div>
))}
</div>
</Section>
</footer>
);
}
+20
View File
@@ -0,0 +1,20 @@
import { Link } from "@tanstack/react-router";
import Logo from "./Logo";
export default function Header() {
return (
<header
id="nav-container"
className="fixed -top-1 w-full h-height-header z-50"
>
<div className="flex flex-row justify-between items-center h-full w-full max-w-max-section m-auto px-section-main-x">
<Link
to="/"
className="w-[120px] h-[120px] flex justify-center items-center rounded-3xl p-2 backdrop-blur-3xl"
>
<Logo />
</Link>
</div>
</header>
);
}
+18
View File
@@ -0,0 +1,18 @@
import type { FC, ReactNode } from "react";
import Footer from "./Footer";
import Header from "./Header";
const Layout: FC<{ children: ReactNode; showHeader?: boolean }> = ({
children,
showHeader = true,
}) => {
return (
<>
{showHeader && <Header />}
<main className="min-h-screen">{children}</main>
<Footer />
</>
);
};
export default Layout;
-97
View File
@@ -1,97 +0,0 @@
---
import Section from "../Section.astro";
enum NavigationItemType {
Link = 0,
Text = 1,
}
type NavigationItem =
| {
path: string;
label: string;
type: NavigationItemType.Link;
}
| {
type: NavigationItemType.Text;
content: string;
};
type NavigationGroup = {
title: string;
class?: string;
items: Array<NavigationItem>;
};
const tree: Array<NavigationGroup> = [
{
title: "Übersicht",
items: [
{
path: "/",
type: NavigationItemType.Link,
label: "Startseite",
},
],
},
{
title: "Rechtliches",
items: [
{
path: "/legal/imprint",
label: "Impressum",
type: NavigationItemType.Link,
},
{
path: "/legal/privacy",
label: "Datenschutzerklärung",
type: NavigationItemType.Link,
},
],
},
{
title: "Zugehöriges",
items: [
{
path: "https://enrico.buehler.earth",
label: "Enrico Bühler",
type: NavigationItemType.Link,
},
],
},
{
title: "",
class: "ml-auto mr-0 self-end",
items: [
{
type: NavigationItemType.Text,
content: "Made with ❤️ in Rottweil",
},
],
},
];
---
<footer class="bg-neutral-accent">
<Section>
<div class="flex flex-row flex-wrap gap-12 w-full pb-8">
{
tree.map((group) => (
<div class={group.class}>
{group.title && <h3 class="mb-2">{group.title}</h3>}
<div class="flex flex-col">
{group.items.map((item) => {
switch (item.type) {
case NavigationItemType.Link:
return <a href={item.path}>{item.label}</a>;
case NavigationItemType.Text:
return <p>{item.content}</p>;
}
})}
</div>
</div>
))
}
</div>
</Section>
</footer>
-16
View File
@@ -1,16 +0,0 @@
---
import Logo from "../Logo/Logo.astro";
---
<header id="nav-container" class="fixed -top-1 w-full h-height-header z-50">
<div
class="flex flex-row justify-between items-center h-full w-full max-w-max-section m-auto px-section-main-x"
>
<a
href="/"
class="w-[120px] h-[120px] flex justify-center items-center rounded-3xl p-2 backdrop-blur-3xl"
>
<Logo />
</a>
</div>
</header>
+12
View File
@@ -0,0 +1,12 @@
import type { FC, ReactNode } from "react";
import Section from "./Section";
const LegalPage: FC<{ children: ReactNode }> = ({ children }) => {
return (
<Section>
<article className="markdown">{children}</article>
</Section>
);
};
export default LegalPage;
@@ -1,28 +1,32 @@
<!--?xml version="1.0" encoding="UTF-8"?-->
<svg
id="Ebene_1"
data-name="Ebene 1"
export default function Logo() {
return (
<svg
aria-label="unom Logo"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 643.6 382.49"
>
>
<title>unom</title>
<path
class="fill-main"
className="fill-main"
d="M128.61,230.82v25.76c-3.39,3.13-15.9,9.57-36.95,9.57s-33.56-6.44-36.95-9.57v-69.51c-6.51-1.15-13.96-1.89-22.4-1.89s-15.9.75-22.4,1.89v71.69c0,30.24,34.38,52.18,81.75,52.18s81.75-21.95,81.75-52.18v-27.9c.54-3.26,10.53-11.58,29.7-16.61v-45.87c-44.16,8.16-74.5,32.3-74.5,62.43Z"
></path>
/>
<path
class="fill-main"
className="fill-main"
d="M552.55,63.76c-27.7,0-50.91,7.57-65.32,19.94-12.48-7.97-28.41-13.93-46.65-17.17.74,3.66,1.14,7.42,1.14,11.28v34.8c19.17,5.03,29.16,13.33,29.7,16.56v52.56c6.51,1.15,13.96,1.89,22.4,1.89s15.9-.75,22.4-1.89v-63.83c3.41-3.08,15.75-9.34,36.33-9.34s32.92,6.26,36.33,9.34v63.83c6.51,1.15,13.96,1.89,22.4,1.89s15.9-.75,22.4-1.89v-66.1c0-30.06-34.12-51.88-81.13-51.88Z"
></path>
/>
<path
class="fill-main"
className="fill-main"
d="M322.42,370.08c-61.63,0-108.1-28.12-108.1-65.41V77.82c0-37.29,46.48-65.41,108.1-65.41s108.1,28.12,108.1,65.41v226.86c0,37.29-46.47,65.41-108.1,65.41ZM322.42,57.2c-41.19,0-62.5,15.85-63.31,20.66v226.81c.81,4.76,22.11,20.61,63.31,20.61s62.5-15.85,63.31-20.66V77.82c-.81-4.76-22.12-20.61-63.31-20.61Z"
></path>
/>
<path
class="fill-main"
className="fill-main"
d="M353.48,72.19c-32.05,10.89-52.91,31.4-53.44,56.03h-.03s0,.96,0,.96v32.77c6.51,1.15,13.96,1.89,22.4,1.89s15.9-.75,22.4-1.89v-32.73c.56-3.27,10.54-11.57,29.7-16.6v-31.07c-2.97-2.53-10.07-6.46-21.03-9.36Z"
></path>
/>
<path
class="fill-main"
className="fill-main"
d="M322.42,290.86c8.44,0,15.9-.75,22.4-1.89v-58.15c0-30.13-30.35-54.26-74.5-62.43v45.87c19.17,5.03,29.16,13.33,29.7,16.56v58.15c6.51,1.15,13.96,1.89,22.4,1.89Z"
></path>
</svg>
/>
</svg>
);
}

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
<defs>
<style>
.cls-1 {
fill: #654eff;
}
.cls-2 {
fill: #fff;
}
</style>
</defs>
<rect class="cls-1" x="87.76" y="87.76" width="824.48" height="824.48" rx="200.39" ry="200.39"/>
<g>
<path class="cls-2" d="M306.81,545.97v25.76c-3.39,3.13-15.9,9.57-36.95,9.57s-33.56-6.44-36.95-9.57v-69.51c-6.51-1.15-13.96-1.89-22.4-1.89s-15.9.75-22.4,1.89v71.69c0,30.24,34.38,52.18,81.75,52.18s81.75-21.95,81.75-52.18v-27.9c.54-3.26,10.53-11.58,29.7-16.61v-45.87c-44.16,8.16-74.5,32.3-74.5,62.43Z"/>
<path class="cls-2" d="M730.75,378.92c-27.7,0-50.91,7.57-65.32,19.94-12.48-7.97-28.41-13.93-46.65-17.17.74,3.66,1.14,7.42,1.14,11.28v34.8c19.17,5.03,29.16,13.33,29.7,16.56v52.56c6.51,1.15,13.96,1.89,22.4,1.89s15.9-.75,22.4-1.89v-63.83c3.41-3.08,15.75-9.34,36.33-9.34s32.92,6.26,36.33,9.34v63.83c6.51,1.15,13.96,1.89,22.4,1.89s15.9-.75,22.4-1.89v-66.1c0-30.06-34.12-51.88-81.13-51.88Z"/>
<path class="cls-2" d="M500.62,685.24c-61.63,0-108.1-28.12-108.1-65.41v-226.86c0-37.29,46.48-65.41,108.1-65.41s108.1,28.12,108.1,65.41v226.86c0,37.29-46.47,65.41-108.1,65.41ZM500.62,372.36c-41.19,0-62.5,15.85-63.31,20.66v226.81c.81,4.76,22.11,20.61,63.31,20.61s62.5-15.85,63.31-20.66v-226.81c-.81-4.76-22.12-20.61-63.31-20.61Z"/>
<path class="cls-2" d="M531.68,387.34c-32.05,10.89-52.91,31.4-53.44,56.03h-.03s0,.96,0,.96v32.77c6.51,1.15,13.96,1.89,22.4,1.89s15.9-.75,22.4-1.89v-32.73c.56-3.27,10.54-11.57,29.7-16.6v-31.07c-2.97-2.53-10.07-6.46-21.03-9.36Z"/>
<path class="cls-2" d="M500.62,606.01c8.44,0,15.9-.75,22.4-1.89v-58.15c0-30.13-30.35-54.26-74.5-62.43v45.87c19.17,5.03,29.16,13.33,29.7,16.56v58.15c6.51,1.15,13.96,1.89,22.4,1.89Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

+28
View File
@@ -0,0 +1,28 @@
export default function LogoQuadBG() {
return (
<svg
aria-label="unom Logo"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1000 1000"
>
<title>unom</title>
<rect
fill="#654eff"
x="87.76"
y="87.76"
width="824.48"
height="824.48"
rx="200.39"
ry="200.39"
/>
<g fill="#fff">
<path d="M306.81,545.97v25.76c-3.39,3.13-15.9,9.57-36.95,9.57s-33.56-6.44-36.95-9.57v-69.51c-6.51-1.15-13.96-1.89-22.4-1.89s-15.9.75-22.4,1.89v71.69c0,30.24,34.38,52.18,81.75,52.18s81.75-21.95,81.75-52.18v-27.9c.54-3.26,10.53-11.58,29.7-16.61v-45.87c-44.16,8.16-74.5,32.3-74.5,62.43Z" />
<path d="M730.75,378.92c-27.7,0-50.91,7.57-65.32,19.94-12.48-7.97-28.41-13.93-46.65-17.17.74,3.66,1.14,7.42,1.14,11.28v34.8c19.17,5.03,29.16,13.33,29.7,16.56v52.56c6.51,1.15,13.96,1.89,22.4,1.89s15.9-.75,22.4-1.89v-63.83c3.41-3.08,15.75-9.34,36.33-9.34s32.92,6.26,36.33,9.34v63.83c6.51,1.15,13.96,1.89,22.4,1.89s15.9-.75,22.4-1.89v-66.1c0-30.06-34.12-51.88-81.13-51.88Z" />
<path d="M500.62,685.24c-61.63,0-108.1-28.12-108.1-65.41v-226.86c0-37.29,46.48-65.41,108.1-65.41s108.1,28.12,108.1,65.41v226.86c0,37.29-46.47,65.41-108.1,65.41ZM500.62,372.36c-41.19,0-62.5,15.85-63.31,20.66v226.81c.81,4.76,22.11,20.61,63.31,20.61s62.5-15.85,63.31-20.66v-226.81c-.81-4.76-22.12-20.61-63.31-20.61Z" />
<path d="M531.68,387.34c-32.05,10.89-52.91,31.4-53.44,56.03h-.03s0,.96,0,.96v32.77c6.51,1.15,13.96,1.89,22.4,1.89s15.9-.75,22.4-1.89v-32.73c.56-3.27,10.54-11.57,29.7-16.6v-31.07c-2.97-2.53-10.07-6.46-21.03-9.36Z" />
<path d="M500.62,606.01c8.44,0,15.9-.75,22.4-1.89v-58.15c0-30.13-30.35-54.26-74.5-62.43v45.87c19.17,5.03,29.16,13.33,29.7,16.56v58.15c6.51,1.15,13.96,1.89,22.4,1.89Z" />
</g>
</svg>
);
}
-27
View File
@@ -1,27 +0,0 @@
---
import type { HTMLAttributes } from "astro/types";
import { cva, type VariantProps } from "class-variance-authority";
const section = cva("relative w-full", {
variants: {
padding: {
true: "px-section-main-x py-section-main-y",
false: "p-0",
},
maxWidth: {
true: "max-w-max-section mx-auto",
false: "",
},
},
});
export interface Props
extends HTMLAttributes<"section">,
VariantProps<typeof section> {}
const { padding = true, maxWidth = true, ...props } = Astro.props;
---
<section {...props} class={section({ padding, maxWidth })}>
<slot />
</section>
+36
View File
@@ -0,0 +1,36 @@
import { cva, type VariantProps } from "class-variance-authority";
import type { HTMLAttributes } from "react";
import { cn } from "@/lib/utils";
const section = cva("relative w-full", {
variants: {
padding: {
true: "px-section-main-x py-section-main-y",
false: "p-0",
},
maxWidth: {
true: "max-w-max-section mx-auto",
false: "",
},
},
defaultVariants: {
padding: true,
maxWidth: true,
},
});
type Props = HTMLAttributes<HTMLElement> & VariantProps<typeof section>;
export default function Section({
padding,
maxWidth,
className,
...props
}: Props) {
return (
<section
{...props}
className={cn(section({ padding, maxWidth }), className)}
/>
);
}
@@ -1,8 +1,3 @@
---
layout: "@/layouts/MarkdownLayout.astro"
title: Impressum
---
# Impressum
Angaben gemäß § 5 TMG
@@ -1,8 +1,3 @@
---
layout: "@/layouts/MarkdownLayout.astro"
title: Datenschutzerklärung
---
# Datenschutz­erklärung
## 1. Datenschutz auf einen Blick
-36
View File
@@ -1,36 +0,0 @@
---
import { cn } from "@/lib/utils.ts";
const anim = {
old: {
name: "pageLeave",
duration: "0.0s",
easing: "ease-out",
fillMode: "forwards",
},
new: {
name: "pageEnter",
duration: "0.7s",
easing: "cubic-bezier(0.23, 1, 0.32, 1)",
fillMode: "forwards",
},
};
export interface Props {
inset?: boolean;
}
const slideAnimation = {
forwards: anim,
backwards: anim,
};
const { inset = true } = Astro.props;
---
<main
transition:animate={slideAnimation}
class={cn("min-h-screen", inset ? "pt-height-header" : "")}
>
<slot />
</main>
-16
View File
@@ -1,16 +0,0 @@
---
import "@/styles/markdown.css";
import Section from "@/components/Section.astro";
import MainLayout from "./MainLayout.astro";
import RootLayout from "./RootLayout.astro";
const { frontmatter } = Astro.props;
---
<RootLayout title={frontmatter.title}>
<MainLayout>
<Section>
<slot />
</Section>
</MainLayout>
</RootLayout>
-38
View File
@@ -1,38 +0,0 @@
---
import Footer from "@/components/Layout/Footer.astro";
import Header from "@/components/Layout/Header.astro";
import "@/styles/global.css";
import "@fontsource/inter";
import { ClientRouter } from "astro:transitions";
export interface Props {
title: string;
showHeader?: boolean;
}
const { title, showHeader = true } = Astro.props;
---
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="description" content="Kreative Webentwicklung aus Rottweil" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<slot name="head" />
<script
is:inline
defer
data-domain="unom.io"
src="https://analytics.unom.io/js/plausible.js"></script>
<meta name="generator" content={Astro.generator} />
<title>unom - {title}</title>
<ClientRouter />
</head>
<body>
{showHeader && <Header transition:name="header" transition:persist />}
<slot />
<Footer />
</body>
</html>
-12
View File
@@ -1,12 +0,0 @@
---
import RootLayout from "@/layouts/RootLayout.astro";
import "../styles/global.css";
import MainLayout from "@/layouts/MainLayout.astro";
import Landing from "@/sections/Landing/Landing.astro";
---
<RootLayout showHeader={false} title="Kreative Webentwicklung">
<MainLayout inset={false}>
<Landing />
</MainLayout>
</RootLayout>
+18
View File
@@ -0,0 +1,18 @@
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
export function getRouter() {
const router = createTanStackRouter({
routeTree,
scrollRestoration: true,
defaultPreload: "intent",
});
return router;
}
declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof getRouter>;
}
}
+54
View File
@@ -0,0 +1,54 @@
import {
createRootRoute,
HeadContent,
Scripts,
} from "@tanstack/react-router";
import "@fontsource/inter";
import Layout from "@/components/Layout";
import appCss from "../styles/globals.css?url";
const SITE_URL = "https://unom.io";
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ name: "description", content: "Kreative Webentwicklung aus Rottweil" },
{ title: "unom - Kreative Webentwicklung" },
{ property: "og:title", content: "unom - Kreative Webentwicklung" },
{
property: "og:description",
content: "Kreative Webentwicklung aus Rottweil",
},
{ property: "og:url", content: SITE_URL },
{ property: "og:type", content: "website" },
],
links: [
{ rel: "stylesheet", href: appCss },
{ rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
],
scripts: [
{
defer: true,
"data-domain": "unom.io",
src: "https://analytics.unom.io/js/plausible.js",
},
],
}),
shellComponent: RootDocument,
});
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="de">
<head>
<HeadContent />
</head>
<body>
<Layout>{children}</Layout>
<Scripts />
</body>
</html>
);
}
+13
View File
@@ -0,0 +1,13 @@
import { createFileRoute } from "@tanstack/react-router";
import Landing from "@/sections/Landing";
export const Route = createFileRoute("/")({
component: HomePage,
head: () => ({
meta: [{ title: "unom - Kreative Webentwicklung" }],
}),
});
function HomePage() {
return <Landing />;
}
+19
View File
@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import ReactMarkdown from "react-markdown";
import LegalPage from "@/components/LegalPage";
import content from "@/content/legal/imprint.md?raw";
export const Route = createFileRoute("/legal/imprint")({
component: ImprintPage,
head: () => ({
meta: [{ title: "unom - Impressum" }],
}),
});
function ImprintPage() {
return (
<LegalPage>
<ReactMarkdown>{content}</ReactMarkdown>
</LegalPage>
);
}
+19
View File
@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import ReactMarkdown from "react-markdown";
import LegalPage from "@/components/LegalPage";
import content from "@/content/legal/privacy.md?raw";
export const Route = createFileRoute("/legal/privacy")({
component: PrivacyPage,
head: () => ({
meta: [{ title: "unom - Datenschutzerklärung" }],
}),
});
function PrivacyPage() {
return (
<LegalPage>
<ReactMarkdown>{content}</ReactMarkdown>
</LegalPage>
);
}
+21
View File
@@ -0,0 +1,21 @@
import LogoQuadBG from "@/components/LogoQuadBG";
import bgDark from "@/assets/unom_Logo_5_Dark.webp";
export default function Landing() {
return (
<div className="w-full h-screen object-cover">
<div className="w-full h-full flex items-center justify-center absolute">
<div className="w-[200px]">
<LogoQuadBG />
</div>
</div>
<img
className="h-full w-full object-cover"
src={bgDark}
width={3840}
height={2160}
alt="Ein 3D Rendering des unom Logos"
/>
</div>
);
}
-20
View File
@@ -1,20 +0,0 @@
---
import { Image } from "astro:assets";
import bgDark from "./unom_Logo_5_Dark.webp";
import LogoQuadBG from "@/components/Logo/LogoQuadBG.astro";
---
<div class="w-full h-screen object-cover">
<div class="w-full h-full flex items-center justify-center absolute">
<div class="w-[200px]">
<LogoQuadBG />
</div>
</div>
<Image
class="h-full object-cover"
src={bgDark}
width={3840}
height={2160}
alt="Ein 3D Rendering des unom Logos"
/>
</div>
+7
View File
@@ -0,0 +1,7 @@
import handler from "@tanstack/react-start/server-entry";
export default {
fetch(req: Request): Promise<Response> {
return handler.fetch(req);
},
};
@@ -1,9 +1,9 @@
@import "./timing-functions.css" layer(base);
@import "./page-transition.css" layer(base);
@import "tailwindcss";
:root {
--main: oklch(100 0 0);
--secondary: oklch(0.85 0 0);
--brand: oklch(0.5609 0.2483 280.67);
--neutral: oklch(0.155 0.0395 285.68);
--neutral-accent: oklch(0.1 0.0395 285.68);
@@ -13,7 +13,8 @@
--font-display: "Ubuntu", ui-sans-serif, system-ui, sans-serif;
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-mono: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
--font-mono:
Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
--radius: 0.625rem;
@@ -51,19 +52,6 @@
--container-max-section: 100%;
}
/* @variant dark {
:root {
--main: oklch(1 0 0);
--secondary: oklch(0.8 0 0);
--primary: oklch(84.44% 0.2131 153.61);
--neutral: oklch(16.36% 0.0088 172.9);
--neutral-accent: oklch(19.73% 0.032 168.99);
--highlight: oklch(66.39% 0.2398 3.2);
--success: oklch(91.1% 0.1605 148.89);
--error: oklch(67.36% 0.2339 0.92);
}
} */
@variant lg {
:root {
--container-max-section: 1000px;
@@ -93,11 +81,7 @@
}
h1 {
@apply text-main font-display;
}
h1 {
@apply text-3xl font-bold;
@apply text-main font-display text-3xl font-bold;
}
h2 {
@@ -120,10 +104,6 @@
@apply decoration-main text-main;
}
[astro-icon] {
fill: currentColor;
}
td {
@apply px-3 py-1 border;
}
@@ -154,4 +134,30 @@
@apply max-w-[600px] text-base text-secondary;
line-height: 1.5;
}
.markdown {
& h1 {
@apply mb-4! mt-8!;
}
& h1,
h2,
h3,
h4,
h5 {
@apply mb-4! mt-6!;
}
& p {
@apply mb-2! whitespace-break-spaces;
}
& ul {
@apply ml-4!;
}
& li {
@apply ml-4!;
}
}
}
-25
View File
@@ -1,25 +0,0 @@
@reference "./global.css";
h1 {
@apply mb-4! mt-8!;
}
h1,
h2,
h3,
h4,
h5 {
@apply mb-4! mt-6!;
}
p {
@apply mb-2! whitespace-break-spaces;
}
ul {
@apply ml-4!;
}
li {
@apply ml-4!;
}
-18
View File
@@ -1,18 +0,0 @@
@keyframes pageEnter {
from {
transform: translateY(40px);
opacity: 0;
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pageLeave {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
+21 -12
View File
@@ -1,18 +1,27 @@
{
"extends": "astro/tsconfigs/strictest",
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"baseUrl": "./src",
"strict": true,
"target": "ES2022",
"jsx": "react-jsx",
"module": "ESNext",
"paths": {
"@/*": ["./*"]
"@/*": ["./src/*"],
"./*": ["./*"]
},
"plugins": [
{
"name": "@astrojs/ts-plugin"
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"allowJs": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
}
],
"jsx": "preserve",
"jsxImportSource": "solid-js"
},
"exclude": ["node_modules", "dist"]
}
+20
View File
@@ -0,0 +1,20 @@
import tailwindcss from "@tailwindcss/vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import { nitro } from "nitro/vite";
import { defineConfig } from "vite";
export default defineConfig({
resolve: {
tsconfigPaths: true,
dedupe: ["react", "react-dom"],
},
plugins: [
tailwindcss(),
tanstackStart(),
nitro({
preset: "bun",
}),
viteReact(),
],
});