chore: add coming soon page
This commit is contained in:
BIN
public/astronaut.png
Normal file
BIN
public/astronaut.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
54
src/middleware.ts
Normal file
54
src/middleware.ts
Normal 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)
|
||||
})
|
||||
96
src/pages/coming-soon.astro
Normal file
96
src/pages/coming-soon.astro
Normal 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
19
src/pages/robots.txt.ts
Normal 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' },
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user