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: on:
push: push:
branches: branches: [main]
- main workflow_dispatch:
pull_request:
types: [opened, synchronize]
jobs: jobs:
deploy: build:
runs-on: ubuntu-latest runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4.2.2
- name: Repository Checkout - name: Set up Docker Buildx
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
env: 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 - name: Log in to Gitea registry
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"
env: env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
timeout-minutes: 10 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 # build output
dist/ dist/
# generated types .output/
.astro/ .nitro/
.tanstack/
# generated router types
src/routeTree.gen.ts
# dependencies # dependencies
node_modules/ node_modules/
@@ -11,7 +15,7 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
bun-debug.log*
# environment variables # environment variables
.env .env
@@ -22,3 +26,6 @@ pnpm-debug.log*
# jetbrains setting folder # jetbrains setting folder
.idea/ .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 ```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) Visit <http://localhost:3000>.
[![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)
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"
+41 -19
View File
@@ -1,20 +1,42 @@
{ {
"name": "@unom/website", "name": "@unom/website",
"type": "module", "private": true,
"version": "0.0.1", "type": "module",
"scripts": { "imports": {
"dev": "astro dev", "#/*": "./src/*"
"build": "astro build", },
"preview": "astro preview", "scripts": {
"astro": "astro" "dev": "vite dev --port 3000 --host",
}, "build": "vite build",
"dependencies": { "start": "bun .output/server/index.mjs",
"@astrojs/mdx": "^4.3.0", "typecheck": "tsc --noEmit",
"@fontsource/inter": "^5.2.5", "format": "biome format",
"@tailwindcss/vite": "^4.1.3", "lint": "biome lint",
"astro": "^5.8.0", "check": "biome check"
"class-variance-authority": "^0.7.1", },
"tailwind-merge": "^3.3.0", "dependencies": {
"tailwindcss": "^4.1.3" "@fontsource/inter": "^5.2.5",
} "@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",
"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"?--> export default function Logo() {
<svg return (
id="Ebene_1" <svg
data-name="Ebene 1" aria-label="unom Logo"
xmlns="http://www.w3.org/2000/svg" role="img"
viewBox="0 0 643.6 382.49" xmlns="http://www.w3.org/2000/svg"
> viewBox="0 0 643.6 382.49"
<path >
class="fill-main" <title>unom</title>
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> className="fill-main"
<path 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"
class="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> className="fill-main"
<path 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"
class="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> className="fill-main"
<path 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"
class="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> className="fill-main"
<path 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"
class="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
></path> className="fill-main"
</svg> 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"
/>
</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 # Impressum
Angaben gemäß § 5 TMG Angaben gemäß § 5 TMG
@@ -45,4 +40,4 @@ Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unt
Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen. Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.
Quelle: Quelle:
<https://www.e-recht24.de> <https://www.e-recht24.de>
@@ -1,8 +1,3 @@
---
layout: "@/layouts/MarkdownLayout.astro"
title: Datenschutzerklärung
---
# Datenschutz­erklärung # Datenschutz­erklärung
## 1. Datenschutz auf einen Blick ## 1. Datenschutz auf einen Blick
@@ -139,4 +134,4 @@ Die Verarbeitung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b DSG
Die von Ihnen an uns per Kontaktanfragen übersandten Daten verbleiben bei uns, bis Sie uns zur Löschung auffordern, Ihre Einwilligung zur Speicherung widerrufen oder der Zweck für die Datenspeicherung entfällt (z. B. nach abgeschlossener Bearbeitung Ihres Anliegens). Zwingende gesetzliche Bestimmungen insbesondere gesetzliche Aufbewahrungsfristen bleiben unberührt. Die von Ihnen an uns per Kontaktanfragen übersandten Daten verbleiben bei uns, bis Sie uns zur Löschung auffordern, Ihre Einwilligung zur Speicherung widerrufen oder der Zweck für die Datenspeicherung entfällt (z. B. nach abgeschlossener Bearbeitung Ihres Anliegens). Zwingende gesetzliche Bestimmungen insbesondere gesetzliche Aufbewahrungsfristen bleiben unberührt.
Quelle: Quelle:
<https://www.e-recht24.de> <https://www.e-recht24.de>
-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 "./timing-functions.css" layer(base);
@import "./page-transition.css" layer(base);
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
--main: oklch(100 0 0); --main: oklch(100 0 0);
--secondary: oklch(0.85 0 0);
--brand: oklch(0.5609 0.2483 280.67); --brand: oklch(0.5609 0.2483 280.67);
--neutral: oklch(0.155 0.0395 285.68); --neutral: oklch(0.155 0.0395 285.68);
--neutral-accent: oklch(0.1 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-display: "Ubuntu", ui-sans-serif, system-ui, sans-serif;
--font-sans: "Inter", 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; Bitstream Vera Sans Mono, Courier New, monospace;
--radius: 0.625rem; --radius: 0.625rem;
@@ -51,19 +52,6 @@
--container-max-section: 100%; --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 { @variant lg {
:root { :root {
--container-max-section: 1000px; --container-max-section: 1000px;
@@ -93,11 +81,7 @@
} }
h1 { h1 {
@apply text-main font-display; @apply text-main font-display text-3xl font-bold;
}
h1 {
@apply text-3xl font-bold;
} }
h2 { h2 {
@@ -120,10 +104,6 @@
@apply decoration-main text-main; @apply decoration-main text-main;
} }
[astro-icon] {
fill: currentColor;
}
td { td {
@apply px-3 py-1 border; @apply px-3 py-1 border;
} }
@@ -151,7 +131,33 @@
p, p,
li { li {
@apply max-w-[600px] text-base text-secondary; @apply max-w-[600px] text-base text-secondary;
line-height: 1.5; 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;
}
}
+25 -16
View File
@@ -1,18 +1,27 @@
{ {
"extends": "astro/tsconfigs/strictest", "include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": { "compilerOptions": {
"baseUrl": "./src", "target": "ES2022",
"strict": true, "jsx": "react-jsx",
"paths": { "module": "ESNext",
"@/*": ["./*"] "paths": {
}, "@/*": ["./src/*"],
"plugins": [ "./*": ["./*"]
{ },
"name": "@astrojs/ts-plugin" "lib": ["ES2022", "DOM", "DOM.Iterable"],
} "types": ["vite/client"],
],
"jsx": "preserve", "moduleResolution": "bundler",
"jsxImportSource": "solid-js" "allowImportingTsExtensions": true,
}, "allowJs": true,
"exclude": ["node_modules", "dist"] "verbatimModuleSyntax": true,
"noEmit": true,
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
}
} }
+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(),
],
});