chore: add coming soon page

This commit is contained in:
Zotov Ivan Yuryevich
2026-06-22 23:12:43 +03:00
parent c630235176
commit 43b399745f
4 changed files with 169 additions and 0 deletions

54
src/middleware.ts Normal file
View File

@@ -0,0 +1,54 @@
import { defineMiddleware } from 'astro:middleware'
// Pre-launch gate: clients unlock the real site via a secret link
// (https://your-domain/?preview=PREVIEW_SECRET); everyone else — strangers and
// bots — is shown src/pages/coming-soon.astro. The gate is active only while
// SHOW_COMING_SOON=true; unset it (or set anything else) to take the site live.
const COOKIE_NAME = 'preview_access'
const COOKIE_MAX_AGE = 60 * 60 * 24 * 7 // 7 days
const COMING_SOON_ROUTE = '/coming-soon'
export const onRequest = defineMiddleware((context, next) => {
// Prefer process.env so values can be set at deploy/runtime (Docker); fall
// back to import.meta.env so local `astro dev` picks them up from .env (Vite
// only loads .env into import.meta.env, not process.env).
const secret = process.env.PREVIEW_SECRET ?? import.meta.env.PREVIEW_SECRET
const showComingSoon =
(process.env.SHOW_COMING_SOON ?? import.meta.env.SHOW_COMING_SOON) === 'true'
// Gate disabled — site is live.
if (!showComingSoon) {
return next()
}
const { cookies, url, redirect } = context
// Magic preview link: ?preview=SECRET remembers the client in a cookie, then
// we redirect to the same URL without the token so it isn't visible/forwarded.
const previewParam = url.searchParams.get('preview')
if (previewParam !== null) {
if (secret && previewParam === secret) {
cookies.set(COOKIE_NAME, secret, {
httpOnly: true,
sameSite: 'lax',
secure: url.protocol === 'https:',
path: '/',
maxAge: COOKIE_MAX_AGE,
})
}
url.searchParams.delete('preview')
return redirect(url.pathname + url.search)
}
// Unlocked clients, the launch page, and robots.txt get the real render
// (robots.txt must reach bots so it can tell them to stay out while gated).
const isClient = Boolean(secret) && cookies.get(COOKIE_NAME)?.value === secret
if (isClient || url.pathname === COMING_SOON_ROUTE || url.pathname === '/robots.txt') {
return next()
}
// Everyone else is shown the launch page while keeping the original URL.
// next(path) rewrites internally without re-running this middleware.
return next(COMING_SOON_ROUTE)
})

View File

@@ -0,0 +1,96 @@
---
// Standalone "Getting ready for launch" page shown to everyone who is NOT an
// unlocked client (strangers, search engines, bots). The gate in
// src/middleware.ts internally rewrites unauthorized requests to this route.
//
// It is intentionally self-contained — no shared Layout and no Strapi data — so
// it renders even while the rest of the site is gated. Edit the copy in <main>.
Astro.response.headers.set('X-Robots-Tag', 'noindex, nofollow')
---
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex, nofollow" />
<title>Скоро запуск</title>
</head>
<body>
<main>
<div class="dot" aria-hidden="true"></div>
<h1>Мы готовимся к запуску</h1>
<p>Сайт скоро станет доступен. Спасибо, что заглянули — мы уже совсем близко.</p>
</main>
<style is:global>
:root {
color-scheme: dark;
}
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
margin: 0;
}
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100%;
padding: 24px;
font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica,
Arial, sans-serif;
color: #e7e9ee;
background: radial-gradient(
1200px 600px at 50% -10%,
#1b2a4a 0%,
#0b1020 55%,
#070a14 100%
);
text-align: center;
}
main {
max-width: 540px;
}
.dot {
width: 12px;
height: 12px;
margin: 0 auto 28px;
border-radius: 50%;
background: #5b8cff;
animation: pulse 2s ease-out infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(91, 140, 255, 0.55);
}
70% {
box-shadow: 0 0 0 18px rgba(91, 140, 255, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(91, 140, 255, 0);
}
}
h1 {
margin: 0 0 14px;
font-size: clamp(28px, 5vw, 40px);
font-weight: 700;
letter-spacing: -0.02em;
}
p {
margin: 0;
font-size: clamp(15px, 2.5vw, 18px);
line-height: 1.6;
color: #a8b0c2;
}
@media (prefers-reduced-motion: reduce) {
.dot {
animation: none;
}
}
</style>
</body>
</html>

19
src/pages/robots.txt.ts Normal file
View File

@@ -0,0 +1,19 @@
import type { APIRoute } from 'astro'
// robots.txt driven by the same SHOW_COMING_SOON flag as the pre-launch gate
// (see src/middleware.ts). While gated, keep the whole site out of search
// indexes; once live, allow crawling. Read at request time so it can be flipped
// via the deploy/runtime environment without a rebuild.
export const prerender = false
export const GET: APIRoute = () => {
const gated =
(process.env.SHOW_COMING_SOON ?? import.meta.env.SHOW_COMING_SOON) === 'true'
const body = gated
? 'User-agent: *\nDisallow: /\n'
: 'User-agent: *\nDisallow:\n'
return new Response(body, {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
})
}