feat(site): pimp homepage with animated 3D background and scroll reveals
Some checks failed
CI / lint (push) Successful in 1m24s
CI / test (push) Successful in 2m24s
CI / validate-json (push) Successful in 57s
CI / markdown-links (push) Successful in 29s
Deploy site / deploy (push) Successful in 7s
Build ISO / build-iso (push) Failing after 14m59s

Adopts the visual feel of Pascal's prototype while keeping Furtka's
voice, brand palette, and bilingual structure intact.

What changed
- Three.js wireframe torus-knot behind the hero, color/opacity tied
  to the existing --accent / --scene-opacity CSS vars so light and
  dark modes both work without a scene re-init.
- Scroll-driven camera zoom + core scale + tilt; canvas opacity fades
  past hero so feature cards stay readable.
- GSAP + ScrollTrigger reveal hero on load and stagger feature cards
  in as they enter the viewport. Lenis smooths scroll.
- "What works today" / "What's coming next" lists move from markdown
  bullets into front-matter arrays and render as scroll-reveal cards
  (7 + 4 cards, EN/DE parallel; copy is 1:1 from the original lists).
- Hero scaled up: gradient text on the wordmark (fg → accent),
  drop-shadow glow in dark mode, brighter lede color.
- Primary CTA -> /releases listing on Forgejo (Forgejo has no
  /releases/latest), with a pulsing glow + arrow slide on hover.
- Version bump 26.8-alpha -> 26.15-alpha to match the actual release.

Performance / a11y
- Vendor JS (Three.js r128, GSAP 3.12.2 + ScrollTrigger, Lenis 1.0.33)
  vendored locally under assets/js/vendor/ - no third-party CDN at
  runtime. ~728 KB total, fingerprinted via Hugo's pipeline with SRI.
- Canvas + scripts gated to homepage only ({{ if .IsHome }}); the
  Impressum/Datenschutz pages stay plain.
- prefers-reduced-motion: scene + GSAP early-return, CSS forces cards
  to their resting state. No-JS users see all content.
- All scripts deferred so first paint isn't blocked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel Maksymilian Syrnicki 2026-04-27 16:14:21 +02:00
parent 1cff22658b
commit aa7dea0528
16 changed files with 464 additions and 61 deletions

1
.gitignore vendored
View file

@ -13,3 +13,4 @@ iso/out/
website/public/ website/public/
website/resources/ website/resources/
website/.hugo_build.lock website/.hugo_build.lock
website/hugo_stats.json

View file

@ -6,6 +6,10 @@
--accent: #c03a28; --accent: #c03a28;
--accent-hover: #a0301f; --accent-hover: #a0301f;
--border: #e4e3dc; --border: #e4e3dc;
--accent-glow: rgba(192, 58, 40, 0.2);
--card-bg: rgba(247, 246, 243, 0.72);
--card-border: var(--border);
--scene-opacity: 0.18;
--font-sans: --font-sans:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, "Noto Sans", sans-serif; Arial, "Noto Sans", sans-serif;
@ -23,6 +27,10 @@
--accent: #ff6b56; --accent: #ff6b56;
--accent-hover: #ff8b78; --accent-hover: #ff8b78;
--border: #232326; --border: #232326;
--accent-glow: rgba(255, 107, 86, 0.4);
--card-bg: rgba(23, 23, 26, 0.65);
--card-border: #26262b;
--scene-opacity: 0.34;
} }
} }
@ -43,6 +51,25 @@ body {
flex-direction: column; flex-direction: column;
min-height: 100vh; min-height: 100vh;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
isolation: isolate;
}
/* ── Animated background canvas (home only) ─────────────── */
.scene-canvas {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
z-index: 0;
pointer-events: none;
}
.site-header,
main.container,
.site-footer {
position: relative;
z-index: 1;
} }
.container { .container {
@ -171,11 +198,36 @@ main.container {
.home h1 { .home h1 {
font-family: var(--font-sans); font-family: var(--font-sans);
font-weight: 800; font-weight: 800;
font-size: clamp(3.25rem, 10vw, 6.5rem); font-size: clamp(3.5rem, 14vw, 11rem);
line-height: 0.95; line-height: 0.9;
letter-spacing: -0.035em; letter-spacing: -0.04em;
margin: 0 0 1.5rem; margin: 0 0 1.5rem;
color: var(--fg); color: var(--fg);
background-image: linear-gradient(180deg, var(--fg) 0%, var(--accent) 110%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
@media (prefers-color-scheme: dark) {
.home h1 {
filter: drop-shadow(0 0 28px var(--accent-glow));
}
.home .lede {
color: #c8c8cc;
}
}
.hero {
min-height: 78vh;
display: flex;
flex-direction: column;
justify-content: center;
padding-block: 4.5rem 3rem;
}
.home .lede {
font-weight: 450;
} }
.home .lede { .home .lede {
@ -258,3 +310,132 @@ main.container {
outline-offset: 3px; outline-offset: 3px;
border-radius: 2px; border-radius: 2px;
} }
/* ── Primary CTA ─────────────────────────────────────────── */
.cta-row { margin-top: 2.5rem; }
.cta {
display: inline-flex;
align-items: center;
gap: 0.55rem;
padding: 1.1rem 2rem;
font-family: var(--font-sans);
font-weight: 600;
font-size: 1.02rem;
letter-spacing: 0.005em;
text-decoration: none;
border-radius: 0.7rem;
transition: transform 180ms, box-shadow 180ms, background 180ms, color 180ms;
}
.cta--primary {
background: linear-gradient(135deg, var(--accent), var(--accent-hover));
color: #fff;
box-shadow: 0 10px 36px var(--accent-glow),
0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent);
animation: cta-pulse 2.8s ease-in-out infinite;
}
.cta--primary:hover {
transform: translateY(-3px);
box-shadow: 0 18px 52px var(--accent-glow),
0 0 0 1px var(--accent);
animation-play-state: paused;
}
.cta--primary:active { transform: translateY(-1px); }
.cta--primary span { transition: transform 180ms; }
.cta--primary:hover span { transform: translateX(4px); }
@keyframes cta-pulse {
0%, 100% { box-shadow: 0 10px 36px var(--accent-glow),
0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent); }
50% { box-shadow: 0 14px 48px var(--accent-glow),
0 0 0 1px color-mix(in srgb, var(--accent) 70%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
.cta--primary { animation: none; }
}
/* ── Intro paragraph (home, between hero and feature grids) ─ */
.intro {
max-width: 38rem;
margin: 0 0 4rem;
font-size: 1.15rem;
line-height: 1.55;
color: var(--fg);
}
.intro p { margin: 0 0 1rem; }
.intro p:last-child { margin: 0; }
.intro strong { font-weight: 600; }
/* ── Feature sections (home) ─────────────────────────────── */
.feature-section { margin-block: 4rem; }
.section-eyebrow {
font-family: var(--font-sans);
font-weight: 500;
font-size: 0.72rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--fg-muted);
margin: 0 0 1.25rem;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
gap: 1rem;
}
.feature-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 1rem;
padding: 1.5rem 1.5rem 1.4rem;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
transition: transform 240ms, border-color 240ms, box-shadow 240ms;
}
.feature-card:hover {
border-color: var(--accent);
box-shadow: 0 10px 32px var(--accent-glow);
transform: translateY(-2px);
}
.feature-card p {
margin: 0;
font-size: 1rem;
line-height: 1.55;
color: var(--fg);
}
.feature-card strong {
font-weight: 600;
color: var(--fg);
}
/* ── Closer prose (home, after feature grids) ────────────── */
.closer {
margin-top: 4rem;
max-width: var(--measure);
}
/* ── Reveal-on-load (hero) and reveal-on-scroll (cards) ──── */
.js .reveal,
.js [data-gsap="card"] {
opacity: 0;
transform: translateY(40px);
will-change: opacity, transform;
}
@media (prefers-reduced-motion: reduce) {
.scene-canvas { display: none; }
.js .reveal,
.js [data-gsap="card"] {
opacity: 1 !important;
transform: none !important;
will-change: auto;
}
}

View file

@ -0,0 +1,25 @@
(function () {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
if (!window.gsap || !window.ScrollTrigger || !window.Lenis) return;
gsap.registerPlugin(ScrollTrigger);
const lenis = new Lenis({ lerp: 0.1, smoothWheel: true });
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((time) => { lenis.raf(time * 1000); });
gsap.ticker.lagSmoothing(0);
// Hero stagger — runs once on load.
gsap.to('.hero .reveal', {
y: 0, opacity: 1, duration: 1.1, ease: 'power3.out', stagger: 0.12
});
// Card reveals — batched so cards in the same row come in together.
ScrollTrigger.batch('[data-gsap="card"]', {
start: 'top 90%',
onEnter: (els) => gsap.to(els, {
y: 0, opacity: 1, scale: 1,
duration: 0.9, ease: 'power3.out', stagger: 0.08, overwrite: true
})
});
})();

View file

@ -0,0 +1,98 @@
(function () {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
if (!window.WebGLRenderingContext || !window.THREE) return;
const canvas = document.getElementById('scene');
if (!canvas) return;
const root = document.documentElement;
const readVar = (name) => getComputedStyle(root).getPropertyValue(name).trim();
const readOpacity = () => parseFloat(readVar('--scene-opacity')) || 0.18;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
60, window.innerWidth / window.innerHeight, 0.1, 100
);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight, false);
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
const geometry = new THREE.TorusKnotGeometry(2.5, 0.4, 130, 20);
const material = new THREE.MeshPhongMaterial({
color: readVar('--accent') || '#c03a28',
wireframe: true,
transparent: true,
opacity: readOpacity()
});
const core = new THREE.Mesh(geometry, material);
scene.add(core);
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const dir = new THREE.DirectionalLight(0xffffff, 0.8);
dir.position.set(5, 5, 5);
scene.add(dir);
const BASE_Z = 9;
camera.position.z = BASE_Z;
let scrollY = window.scrollY || 0;
window.addEventListener('scroll', () => {
scrollY = window.scrollY || 0;
}, { passive: true });
let baseOpacity = readOpacity();
let running = true;
function tick() {
if (!running) return;
requestAnimationFrame(tick);
// Continuous slow drift.
core.rotation.y += 0.0015;
core.rotation.z += 0.0006;
// Scroll-driven motion: zoom in, scale up, tilt.
const s = Math.min(scrollY, 2000);
camera.position.z = BASE_Z - s * 0.0022;
const scale = 1 + s * 0.00035;
core.scale.set(scale, scale, scale);
core.rotation.x = s * 0.0008;
// Fade past hero so feature cards stay readable.
const vh = window.innerHeight;
const fadeStart = vh * 0.5;
const fadeEnd = vh * 1.4;
const t = Math.max(0, Math.min(1, (scrollY - fadeStart) / (fadeEnd - fadeStart)));
material.opacity = baseOpacity * (1 - t * 0.92);
renderer.render(scene, camera);
}
tick();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight, false);
});
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
running = false;
} else if (!running) {
running = true;
tick();
}
});
const mql = window.matchMedia('(prefers-color-scheme: dark)');
const updateTheme = () => {
const accent = readVar('--accent');
if (accent) material.color.set(accent);
baseOpacity = readOpacity();
};
if (mql.addEventListener) {
mql.addEventListener('change', updateTheme);
} else if (mql.addListener) {
mql.addListener(updateTheme);
}
})();

19
website/assets/js/vendor/PROVENANCE.md vendored Normal file
View file

@ -0,0 +1,19 @@
# Vendored JavaScript libraries
These minified bundles are checked into the repo so furtka.org has zero
third-party-CDN dependencies at runtime. Pin date: **2026-04-27**.
| File | Version | Source |
|---|---|---|
| `three.min.js` | r128 | https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js |
| `gsap.min.js` | 3.12.2 (core only) | https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js |
| `ScrollTrigger.min.js` | 3.12.2 | https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js |
| `lenis.min.js` | @studio-freight/lenis 1.0.33 | https://unpkg.com/@studio-freight/lenis@1.0.33/dist/lenis.min.js |
All four expose UMD globals (`THREE`, `gsap`, `ScrollTrigger`, `Lenis`).
None are ES modules, so no `js.Build` step is needed — Hugo just fingerprints them.
GSAP "Club" plugins (SplitText, MorphSVG, etc.) are **not** free for commercial use.
Only `gsap` core + `ScrollTrigger` (both standard MIT-style license) are bundled.
To refresh: re-run `curl -sSfL -o <file> <url>` and bump the pin date here.

File diff suppressed because one or more lines are too long

11
website/assets/js/vendor/gsap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
website/assets/js/vendor/lenis.min.js vendored Normal file

File diff suppressed because one or more lines are too long

6
website/assets/js/vendor/three.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,33 +1,33 @@
--- ---
title: "Furtka" title: "Furtka"
description: "Offenes Heimserver-Betriebssystem — einfach genug für alle." description: "Offenes Heimserver-Betriebssystem — einfach genug für alle."
status: "<span class=\"mono\">26.8-alpha</span> — in Arbeit" status: "<span class=\"mono\">26.15-alpha</span> — in Arbeit"
# features_today / features_next müssen index-parallel zu content/_index.md bleiben.
intro: |
**Furtka** ist ein offenes Heimserver-Betriebssystem.
USB-Stick einstecken, durch einen Assistenten klicken, und aus jedem
alten Rechner wird eine private Cloud für den Haushalt — mit eigenen
Apps, eigenem Namen im Netz, eigenen Daten.
Das Ziel ist einfach: **dein Vater soll das einrichten können.**
features_today_label: "Was heute schon geht"
features_today:
- "Vom USB-Stick booten und Furtka auf die Festplatte einrichten"
- "Ein Assistent fragt nach Name, Benutzer und Netzwerk — fertig"
- "Danach: Bedienseite im Browser öffnen"
- "Erste App: **Dateifreigabe im Heimnetz** (alle im WLAN sehen den Ordner)"
- "Apps mit einem Klick installieren und entfernen"
- "Eine installierte App mit einem Klick aktualisieren (holt das neueste Container-Image)"
- "Furtka selbst mit einem Klick aktualisieren — keine Neuinstallation mehr für neue Features"
features_next_label: "Was als Nächstes kommt"
features_next:
- "Apps für Fotos, Dateien, Smarthome, Spiele-Streaming und Medien"
- "Einfachere Sprache im Einrichtungs-Assistenten"
- "Sichere Verbindung im Heimnetz (ohne Warnmeldung im Browser)"
- "Mehrere Server zusammenschalten"
--- ---
**Furtka** ist ein offenes Heimserver-Betriebssystem.
USB-Stick einstecken, durch einen Assistenten klicken, und aus jedem
alten Rechner wird eine private Cloud für den Haushalt — mit eigenen
Apps, eigenem Namen im Netz, eigenen Daten.
Das Ziel ist einfach: **dein Vater soll das einrichten können.**
### Was als Nächstes kommt
- Apps für Fotos, Dateien, Smarthome, Spiele-Streaming und Medien
- Einfachere Sprache im Einrichtungs-Assistenten
- Sichere Verbindung im Heimnetz (ohne Warnmeldung im Browser)
- Mehrere Server zusammenschalten
### Was heute schon geht
- Vom USB-Stick booten und Furtka auf die Festplatte einrichten
- Ein Assistent fragt nach Name, Benutzer und Netzwerk — fertig
- Danach: Bedienseite im Browser öffnen
- Erste App: **Dateifreigabe im Heimnetz** (alle im WLAN sehen den Ordner)
- Apps mit einem Klick installieren und entfernen
- Eine installierte App mit einem Klick aktualisieren (holt das neueste Container-Image)
- Furtka selbst mit einem Klick aktualisieren — keine Neuinstallation mehr für neue Features
Wir sind zu zweit und bauen das öffentlich, abends und am Wochenende. Wir sind zu zweit und bauen das öffentlich, abends und am Wochenende.
Es ist früh. Es ist früh.
Mitlesen? Schreib an <a href="mailto:hallo@furtka.org">hallo@furtka.org</a>. Mitlesen? Schreib an <hallo@furtka.org>.

View file

@ -1,33 +1,33 @@
--- ---
title: "Furtka" title: "Furtka"
description: "Open-source home server OS — simple enough for everyone." description: "Open-source home server OS — simple enough for everyone."
status: "<span class=\"mono\">26.8-alpha</span> — work in progress" status: "<span class=\"mono\">26.15-alpha</span> — work in progress"
# Keep features_today / features_next index-aligned with content/_index.de.md.
intro: |
**Furtka** is an open-source home server OS.
Boot from USB, click through a wizard, and any old computer
turns into a private cloud for your household — with your own apps,
your own name on the network, your own data.
The goal is simple: **your dad should be able to set this up.**
features_today_label: "What works today"
features_today:
- "Boot from USB stick and install Furtka onto the hard drive"
- "A wizard asks for name, user and network — done"
- "Then: open the control page in your browser"
- "First app: **file sharing on the home network** (everyone on Wi-Fi sees the folder)"
- "Install and remove apps with one click"
- "Update an installed app with one click (pulls the newest container image)"
- "Update Furtka itself with one click — no reinstalling for new features"
features_next_label: "What's coming next"
features_next:
- "Apps for photos, files, smart home, game streaming and media"
- "Plainer language in the setup wizard"
- "Secure connection on your home network (no browser warning)"
- "Linking several servers together"
--- ---
**Furtka** is an open-source home server OS.
Boot from USB, click through a wizard, and any old computer
turns into a private cloud for your household — with your own apps,
your own name on the network, your own data.
The goal is simple: **your dad should be able to set this up.**
### What's coming next
- Apps for photos, files, smart home, game streaming and media
- Plainer language in the setup wizard
- Secure connection on your home network (no browser warning)
- Linking several servers together
### What works today
- Boot from USB stick and install Furtka onto the hard drive
- A wizard asks for name, user and network — done
- Then: open the control page in your browser
- First app: **file sharing on the home network** (everyone on Wi-Fi sees the folder)
- Install and remove apps with one click
- Update an installed app with one click (pulls the newest container image)
- Update Furtka itself with one click — no reinstalling for new features
We're two people building it in public on evenings and weekends. We're two people building it in public on evenings and weekends.
It's early. It's early.
Want to follow along? Write to <a href="mailto:hallo@furtka.org">hallo@furtka.org</a>. Want to follow along? Write to <hallo@furtka.org>.

View file

@ -6,7 +6,7 @@ enableRobotsTXT = true
[params] [params]
description = "Open-source home server OS — simple enough for everyone." description = "Open-source home server OS — simple enough for everyone."
version = "26.8-alpha" version = "26.15-alpha"
contactEmail = "hallo@furtka.org" contactEmail = "hallo@furtka.org"
[markup.goldmark.renderer] [markup.goldmark.renderer]

View file

@ -1,13 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ .Site.Language.Lang }}"> <html lang="{{ .Site.Language.Lang }}" class="no-js">
<head> <head>
{{ partial "head.html" . }} {{ partial "head.html" . }}
</head> </head>
<body> <body>
{{ if .IsHome }}<canvas id="scene" class="scene-canvas" aria-hidden="true"></canvas>{{ end }}
{{ partial "header.html" . }} {{ partial "header.html" . }}
<main class="container"> <main class="container">
{{ block "main" . }}{{ end }} {{ block "main" . }}{{ end }}
</main> </main>
{{ partial "footer.html" . }} {{ partial "footer.html" . }}
{{ if .IsHome }}{{ partial "scripts.html" . }}{{ end }}
</body> </body>
</html> </html>

View file

@ -2,13 +2,46 @@
<article class="home"> <article class="home">
<header class="hero"> <header class="hero">
{{ with .Params.status }} {{ with .Params.status }}
<p class="status-chip">{{ . | safeHTML }}</p> <p class="status-chip reveal">{{ . | safeHTML }}</p>
{{ end }} {{ end }}
<h1>{{ .Title }}</h1> <h1 class="reveal">{{ .Title }}</h1>
{{ with site.Params.description }}<p class="lede">{{ . }}</p>{{ end }} {{ with site.Params.description }}<p class="lede reveal">{{ . }}</p>{{ end }}
<p class="cta-row reveal">
<a class="cta cta--primary" href="https://forgejo.sourcegate.online/daniel/furtka/releases">
{{ if eq site.Language.Lang "de" }}Neuestes Release{{ else }}Latest release{{ end }}
<span aria-hidden="true"></span>
</a>
</p>
</header> </header>
<div class="prose">
{{ .Content }} {{ with .Params.intro }}
</div> <section class="intro">{{ . | markdownify }}</section>
{{ end }}
{{ if .Params.features_today }}
<section class="feature-section">
{{ with .Params.features_today_label }}<p class="section-eyebrow">{{ . }}</p>{{ end }}
<div class="feature-grid">
{{ range .Params.features_today }}
<article class="feature-card" data-gsap="card">{{ . | markdownify }}</article>
{{ end }}
</div>
</section>
{{ end }}
{{ if .Params.features_next }}
<section class="feature-section">
{{ with .Params.features_next_label }}<p class="section-eyebrow">{{ . }}</p>{{ end }}
<div class="feature-grid">
{{ range .Params.features_next }}
<article class="feature-card" data-gsap="card">{{ . | markdownify }}</article>
{{ end }}
</div>
</section>
{{ end }}
{{ with .Content }}
<section class="prose closer">{{ . }}</section>
{{ end }}
</article> </article>
{{ end }} {{ end }}

View file

@ -1,7 +1,10 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<script>document.documentElement.classList.replace('no-js','js');</script>
<title>{{ if .IsHome }}{{ site.Title }} — {{ site.Params.description }}{{ else }}{{ .Title }} · {{ site.Title }}{{ end }}</title> <title>{{ if .IsHome }}{{ site.Title }} — {{ site.Params.description }}{{ else }}{{ .Title }} · {{ site.Title }}{{ end }}</title>
<meta name="description" content="{{ with .Params.description }}{{ . }}{{ else }}{{ site.Params.description }}{{ end }}"> <meta name="description" content="{{ with .Params.description }}{{ . }}{{ else }}{{ site.Params.description }}{{ end }}">
<meta name="theme-color" content="#f7f6f3" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0d0d0f" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/svg+xml" href="/favicon.svg"> <link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta property="og:site_name" content="{{ site.Title }}"> <meta property="og:site_name" content="{{ site.Title }}">
<meta property="og:title" content="{{ if .IsHome }}{{ site.Title }}{{ else }}{{ .Title }} · {{ site.Title }}{{ end }}"> <meta property="og:title" content="{{ if .IsHome }}{{ site.Title }}{{ else }}{{ .Title }} · {{ site.Title }}{{ end }}">

View file

@ -0,0 +1,12 @@
{{ $three := resources.Get "js/vendor/three.min.js" | fingerprint }}
{{ $gsap := resources.Get "js/vendor/gsap.min.js" | fingerprint }}
{{ $st := resources.Get "js/vendor/ScrollTrigger.min.js" | fingerprint }}
{{ $lenis := resources.Get "js/vendor/lenis.min.js" | fingerprint }}
{{ $scene := resources.Get "js/scene.js" | fingerprint }}
{{ $anim := resources.Get "js/animations.js" | fingerprint }}
<script defer src="{{ $three.RelPermalink }}" integrity="{{ $three.Data.Integrity }}"></script>
<script defer src="{{ $gsap.RelPermalink }}" integrity="{{ $gsap.Data.Integrity }}"></script>
<script defer src="{{ $st.RelPermalink }}" integrity="{{ $st.Data.Integrity }}"></script>
<script defer src="{{ $lenis.RelPermalink }}" integrity="{{ $lenis.Data.Integrity }}"></script>
<script defer src="{{ $scene.RelPermalink }}" integrity="{{ $scene.Data.Integrity }}"></script>
<script defer src="{{ $anim.RelPermalink }}" integrity="{{ $anim.Data.Integrity }}"></script>