Szablon witryny, docker i workflow gitea

This commit is contained in:
dm
2025-11-20 21:22:37 +01:00
commit 1e5f2bc38a
30 changed files with 601 additions and 0 deletions

11
.env.example Normal file
View File

@@ -0,0 +1,11 @@
# Public URL of the website
PUBLIC_SITE_URL=https://www.fuz.dariuszm.eu
# Google Maps
PUBLIC_GOOGLE_MAPS_KEY=
# Contact form API
FORMS_ENDPOINT=
# Astro SSR
NODE_ENV=production

View File

@@ -0,0 +1,27 @@
name: Deploy FUZ 2.0 to Contabo
on:
push:
branches:
- main
jobs:
deploy:
runs-on: contabo-runner
steps:
- name: Pull latest code
run: |
cd /opt/fuz-site
git reset --hard HEAD
git pull
- name: Build image
run: |
cd /opt/fuz-site
docker compose -f docker-compose.prod.yml build
- name: Restart service
run: |
cd /opt/fuz-site
docker compose -f docker-compose.prod.yml up -d

44
.gitignore vendored Normal file
View File

@@ -0,0 +1,44 @@
# Node
node_modules/
npm-debug.log*
yarn-debug.log*
pnpm-debug.log*
package-lock.json
pnpm-lock.yaml
# Astro
dist/
.astro/
.build/
# Logs
*.log
logs/
# Env
.env
.env.*
!.env.example
# OS
.DS_Store
Thumbs.db
# Editor
.vscode/
.idea/
# Docker
docker-data/
docker-cache/
# Temp
tmp/
temp/
*.tmp
# Public generated
public/**/*.avif
public/**/*.webp
public/og/*.txt

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
# ============================
# 1) BUILD STAGE
# ============================
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# ============================
# 2) RUNTIME STAGE (alpine)
# ============================
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app /app
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/server/entry.mjs"]

12
astro.config.mjs Normal file
View File

@@ -0,0 +1,12 @@
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
export default defineConfig({
srcDir: 'src',
output: 'server',
integrations: [
tailwind({
applyBaseStyles: true
})
]
});

19
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,19 @@
version: "3.9"
services:
fuz-site:
build:
context: /opt/fuz-site
dockerfile: Dockerfile
container_name: fuz-site
restart: unless-stopped
env_file:
- .env
ports:
- "4000:3000"
networks:
- n8n_default
networks:
n8n_default:
external: true

20
package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "fuz20",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"dependencies": {
"astro": "^4.0.0",
"js-yaml": "^4.1.0"
},
"devDependencies": {
"@astrojs/tailwind": "^5.0.0",
"tailwindcss": "^3.4.0",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.0"
}
}

6
postcss.config.cjs Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

4
public/favicon.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="12" fill="#0ea5e9"/>
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="central" font-size="32" fill="white" font-family="system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">F</text>
</svg>

After

Width:  |  Height:  |  Size: 317 B

View File

@@ -0,0 +1,63 @@
---
const {
title = [],
subtitle = [],
description,
imageUrl,
ctas = []
} = Astro.props;
---
<section class="fuz-section">
<div class="fuz-container grid md:grid-cols-2 gap-10 items-center">
<div class="space-y-6">
{Array.isArray(title) ? (
<h1 class="fuz-hero-title">
{title.map((line) => (
<span class="block">{line}</span>
))}
</h1>
) : (
<h1 class="fuz-hero-title">{title}</h1>
)}
{Array.isArray(subtitle) && subtitle.length > 0 && (
<div class="fuz-hero-subtitle space-y-1">
{subtitle.map((line) => (
<p>{line}</p>
))}
</div>
)}
{description && (
<p class="mt-4 text-base md:text-lg text-gray-600 dark:text-gray-300 max-w-xl">
{description}
</p>
)}
{ctas.length > 0 && (
<div class="mt-6 flex flex-wrap gap-3">
{ctas.map((cta) => (
<a
href={cta.href}
class="inline-flex items-center rounded-full px-5 py-2.5 text-sm font-medium bg-sky-600 text-white hover:bg-sky-700 dark:bg-sky-500 dark:hover:bg-sky-400"
>
{cta.label}
</a>
))}
</div>
)}
</div>
{imageUrl && (
<div class="relative aspect-video md:aspect-[4/3] overflow-hidden rounded-3xl border border-slate-200/70 dark:border-slate-700/70">
<img
src={imageUrl}
alt=""
loading="lazy"
class="h-full w-full object-cover"
/>
</div>
)}
</div>
</section>

View File

@@ -0,0 +1,10 @@
<footer class="fuz-footer">
<div class="fuz-container flex flex-col md:flex-row items-center justify-between gap-3">
<p class="fuz-footer-text">
© {new Date().getFullYear()} FUZ lokalny operator internetu i telewizji.
</p>
<p class="text-xs text-gray-400">
RODO • Polityka prywatności • Cookies
</p>
</div>
</footer>

View File

@@ -0,0 +1,29 @@
---
const nav = [
{ href: "/", label: "Strona główna" },
{ href: "/internet", label: "Internet" },
{ href: "/telewizja", label: "Telewizja" },
{ href: "/internet-telewizja", label: "Internet + TV" },
{ href: "/mapa-zasiegu", label: "Mapa zasięgu" },
{ href: "/kontakt", label: "Kontakt" }
];
---
<header class="fuz-header">
<div class="fuz-container flex items-center justify-between py-3">
<a href="/" class="flex items-center gap-2 font-semibold">
<span class="inline-flex h-8 w-8 items-center justify-center rounded-full bg-sky-500 text-white text-sm font-bold">
F
</span>
<span>FUZ</span>
</a>
<nav class="hidden md:flex items-center gap-6 text-sm">
{nav.map((item) => (
<a href={item.href} class="hover:text-sky-600 dark:hover:text-sky-400">
{item.label}
</a>
))}
</nav>
</div>
</header>

View File

@@ -0,0 +1,21 @@
---
const { href, variant = "primary" } = Astro.props;
const base =
"inline-flex items-center justify-center rounded-full px-4 py-2 text-sm font-medium transition";
const variants = {
primary: "bg-sky-600 text-white hover:bg-sky-700 dark:bg-sky-500 dark:hover:bg-sky-400",
secondary: "bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-100"
};
const classes = `${base} ${variants[variant] ?? variants.primary}`;
---
{href ? (
<a href={href} class={classes}>
<slot />
</a>
) : (
<button type="button" class={classes}>
<slot />
</button>
)}

View File

@@ -0,0 +1,15 @@
---
const { message, type = "success" } = Astro.props;
const base = "fixed right-4 top-20 z-50 rounded-xl px-4 py-3 text-sm shadow-lg border backdrop-blur";
const variants = {
success: "bg-emerald-50/90 border-emerald-200 text-emerald-900 dark:bg-emerald-900/70 dark:border-emerald-700 dark:text-emerald-50",
error: "bg-rose-50/90 border-rose-200 text-rose-900 dark:bg-rose-900/70 dark:border-rose-700 dark:text-rose-50",
info: "bg-sky-50/90 border-sky-200 text-sky-900 dark:bg-sky-900/70 dark:border-sky-700 dark:text-sky-50"
};
const classes = `${base} ${variants[type] ?? variants.success}`;
---
<div class={classes}>
{message}
</div>

View File

@@ -0,0 +1,13 @@
title:
- "Dlaczego FUZ?"
subtitle:
- "Lokalny operator znamy Twoją okolicę"
- "Realny serwis, szybkie wsparcie"
- "Stabilna infrastruktura światłowodowa i radiowa"
description: "Internet i telewizja od ludzi, którzy naprawdę są na miejscu. Bez infolinii z końca świata."
imageUrl: "/images/hero/fiber-example.jpg"
ctas:
- label: "Sprawdź dostępność"
href: "/mapa-zasiegu"
- label: "Zobacz ofertę"
href: "/oferta"

16
src/content/seo/home.yaml Normal file
View File

@@ -0,0 +1,16 @@
title: "FUZ Internet i telewizja w Twojej okolicy"
description: "Lokalny operator internetu i telewizji. Nowoczesny światłowód i radio, realny serwis i szybkie wsparcie."
canonical: "/"
image: "/og/fuz-home.png"
keywords:
- internet
- światłowód
- telewizja
- lokalny operator
schema:
"@context": "https://schema.org"
"@type": "Organization"
name: "FUZ"
url: "https://www.fuz.pl"
sameAs:
- "https://www.facebook.com"

1
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference path="../.astro/types.d.ts" />

View File

@@ -0,0 +1,43 @@
---
const { seo } = Astro.props;
const {
title = "FUZ",
description = "Lokalny operator internetu i telewizji.",
canonical = "/",
image = "/og/default.png",
keywords = [],
schema = {}
} = seo ?? {};
---
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="description" content={description} />
{keywords.length > 0 && (
<meta name="keywords" content={keywords.join(", ")} />
)}
<link rel="canonical" href={canonical} />
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
{Object.keys(schema).length > 0 && (
<script type="application/ld+json">
{JSON.stringify(schema)}
</script>
)}
<slot />
</head>

View File

@@ -0,0 +1,22 @@
---
import "../styles/base.css";
import BaseHead from "./BaseHead.astro";
import Header from "../components/layout/Header.astro";
import Footer from "../components/layout/Footer.astro";
const { seo } = Astro.props;
---
<html lang="pl" class="scroll-smooth">
<BaseHead seo={seo} />
<body class="min-h-screen flex flex-col">
<Header />
<main class="flex-1">
<slot />
</main>
<Footer />
</body>
</html>

14
src/pages/index.astro Normal file
View File

@@ -0,0 +1,14 @@
---
import DefaultLayout from "../layouts/DefaultLayout.astro";
import Hero from "../components/hero/Hero.astro";
import yaml from "js-yaml";
import fs from "fs";
const seo = yaml.load(fs.readFileSync("./src/content/seo/home.yaml", "utf8"));
const hero = yaml.load(fs.readFileSync("./src/content/home/hero.yaml", "utf8"));
---
<DefaultLayout seo={seo}>
<Hero {...hero} />
</DefaultLayout>

View File

@@ -0,0 +1,20 @@
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
const seo = {
title: "Internet + Telewizja FUZ",
description: "Internet + Telewizja FUZ",
canonical: "/internet-telewizja"
};
---
<DefaultLayout seo={seo}>
<section class="fuz-section">
<div class="fuz-container">
<h1 class="fuz-hero-title">Internet + Telewizja FUZ</h1>
<p class="mt-4 text-gray-600 dark:text-gray-300">
Ta podstrona jest na razie szkieletem. Możemy tu później wczytać treść z YAML.
</p>
</div>
</section>
</DefaultLayout>

View File

@@ -0,0 +1,20 @@
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
const seo = {
title: "Internet FUZ",
description: "Internet FUZ",
canonical: "/internet"
};
---
<DefaultLayout seo={seo}>
<section class="fuz-section">
<div class="fuz-container">
<h1 class="fuz-hero-title">Internet FUZ</h1>
<p class="mt-4 text-gray-600 dark:text-gray-300">
Ta podstrona jest na razie szkieletem. Możemy tu później wczytać treść z YAML.
</p>
</div>
</section>
</DefaultLayout>

View File

@@ -0,0 +1,20 @@
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
const seo = {
title: "Kontakt FUZ",
description: "Kontakt FUZ",
canonical: "/kontakt"
};
---
<DefaultLayout seo={seo}>
<section class="fuz-section">
<div class="fuz-container">
<h1 class="fuz-hero-title">Kontakt FUZ</h1>
<p class="mt-4 text-gray-600 dark:text-gray-300">
Ta podstrona jest na razie szkieletem. Możemy tu później wczytać treść z YAML.
</p>
</div>
</section>
</DefaultLayout>

View File

@@ -0,0 +1,20 @@
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
const seo = {
title: "Mapa zasięgu FUZ",
description: "Mapa zasięgu FUZ",
canonical: "/mapa-zasiegu"
};
---
<DefaultLayout seo={seo}>
<section class="fuz-section">
<div class="fuz-container">
<h1 class="fuz-hero-title">Mapa zasięgu FUZ</h1>
<p class="mt-4 text-gray-600 dark:text-gray-300">
Ta podstrona jest na razie szkieletem. Możemy tu później wczytać treść z YAML.
</p>
</div>
</section>
</DefaultLayout>

View File

@@ -0,0 +1,20 @@
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
const seo = {
title: "Oferta FUZ",
description: "Oferta FUZ",
canonical: "/oferta"
};
---
<DefaultLayout seo={seo}>
<section class="fuz-section">
<div class="fuz-container">
<h1 class="fuz-hero-title">Oferta FUZ</h1>
<p class="mt-4 text-gray-600 dark:text-gray-300">
Ta podstrona jest na razie szkieletem. Możemy tu później wczytać treść z YAML.
</p>
</div>
</section>
</DefaultLayout>

View File

@@ -0,0 +1,20 @@
---
import DefaultLayout from "../../layouts/DefaultLayout.astro";
const seo = {
title: "Telewizja FUZ",
description: "Telewizja FUZ",
canonical: "/telewizja"
};
---
<DefaultLayout seo={seo}>
<section class="fuz-section">
<div class="fuz-container">
<h1 class="fuz-hero-title">Telewizja FUZ</h1>
<p class="mt-4 text-gray-600 dark:text-gray-300">
Ta podstrona jest na razie szkieletem. Możemy tu później wczytać treść z YAML.
</p>
</div>
</section>
</DefaultLayout>

1
src/styles/base.css Normal file
View File

@@ -0,0 +1 @@
@import "./tailwind.css";

36
src/styles/tailwind.css Normal file
View File

@@ -0,0 +1,36 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
scroll-behavior: smooth;
}
body {
@apply bg-white text-gray-900 dark:bg-slate-900 dark:text-gray-100 antialiased;
}
/* Simple FUZ section helpers */
.fuz-section {
@apply py-16 md:py-24;
}
.fuz-container {
@apply container mx-auto px-4;
}
.fuz-hero-title {
@apply text-3xl md:text-5xl font-bold tracking-tight;
}
.fuz-hero-subtitle {
@apply mt-4 text-lg md:text-xl text-gray-600 dark:text-gray-300;
}
.fuz-header {
@apply border-b border-slate-200/60 dark:border-slate-700/60 bg-white/70 dark:bg-slate-900/70 backdrop-blur;
}
.fuz-footer {
@apply border-t border-slate-200/60 dark:border-slate-700/60 py-6 mt-10 text-sm text-gray-500 dark:text-gray-400;
}

15
tailwind.config.cjs Normal file
View File

@@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{astro,html,js,jsx,ts,tsx,vue}"
],
theme: {
extend: {
container: {
center: true,
padding: "1rem"
}
}
},
plugins: []
};

12
tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@layouts/*": ["src/layouts/*"],
"@components/*": ["src/components/*"],
"@styles/*": ["src/styles/*"],
"@content/*": ["src/content/*"]
}
}
}