This commit is contained in:
2026-06-13 17:23:18 +03:30
parent 53b8a91843
commit 2c8a134d41
5 changed files with 1549 additions and 2 deletions

View File

@@ -1,2 +1,39 @@
# nais # NAIS
Nginx Autoindex Skin
NAIS is a styled nginx autoindex page with convenient playlist download and
link-copying actions.
[Website](https://meghdadfadaee.github.io/nais/) ·
[GitHub](https://github.com/meghdadfadaee/nais)
## Installation
The interactive installer requires nginx, curl, root access through either the
current user or `sudo`, and an nginx service managed by `systemctl` or
`service`:
```sh
curl -fsSL https://meghdadfadaee.github.io/nais/setup.sh | sh
```
It asks for:
- `server_name`: one hostname or IP address.
- `root_path`: the absolute path whose files nginx should list.
- `assets_path`: where the NAIS CSS, JavaScript, and favicon are installed.
The default is `/var/www/nais-assets`.
- `certificates_path`: an optional directory containing `fullchain.pem` and
`privkey.pem`.
Without a certificates path, the generated site serves HTTP on port 80. With a
certificates path, it serves HTTPS on port 443 and redirects HTTP to HTTPS.
The installer detects common nginx virtual-host include directories. If none is
configured, it adds a managed `nais-sites/*.conf` include to nginx's main
configuration. It validates changes with `nginx -t` before reloading or starting
nginx. Re-running the installer refreshes the assets and replaces the managed
NAIS site configuration.
The installer does not install nginx or obtain TLS certificates.
The nginx worker user must have permission to traverse and read `root_path`; the
installer does not change permissions on existing content.

184
index.html Normal file
View File

@@ -0,0 +1,184 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#0f111a">
<meta name="description" content="NAIS gives nginx autoindex a clean, modern interface with one-command setup.">
<meta property="og:title" content="NAIS - Nginx Autoindex Skin">
<meta property="og:description" content="A modern, lightweight interface for nginx autoindex.">
<meta property="og:type" content="website">
<meta property="og:image" content="https://meghdadfadaee.github.io/nais/favicon.png">
<title>NAIS - Nginx Autoindex Skin</title>
<link rel="icon" href="./favicon.ico">
<link rel="stylesheet" href="./landing.css">
<script src="./landing.js" defer></script>
</head>
<body>
<div class="ambient ambient-one" aria-hidden="true"></div>
<div class="ambient ambient-two" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="./" aria-label="NAIS home">
<img src="./favicon.png" alt="" width="42" height="42">
<span>NAIS</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="#features">Features</a>
<a href="#install">Install</a>
<a class="github-link" href="https://github.com/meghdadfadaee/nais">GitHub</a>
</nav>
</header>
<main>
<section class="hero">
<div class="hero-copy">
<div class="eyebrow"><span></span> Nginx autoindex, refined</div>
<h1>Your files.<br><strong>Better presented.</strong></h1>
<p class="hero-text">
NAIS turns the default nginx directory listing into a fast,
focused, and modern file browser without changing how nginx works.
</p>
<div class="hero-actions">
<a class="button button-primary" href="#install">Install NAIS</a>
<a class="button button-secondary" href="https://github.com/meghdadfadaee/nais">
View source
</a>
</div>
<div class="quick-command">
<code>curl -fsSL https://meghdadfadaee.github.io/nais/setup.sh | sh</code>
<button class="copy-button" type="button" data-copy-command aria-label="Copy install command">
<span class="copy-label">Copy</span>
</button>
</div>
</div>
<div class="preview-wrap" id="preview" aria-label="Preview of the NAIS file browser">
<div class="preview-glow" aria-hidden="true"></div>
<div class="browser">
<div class="browser-bar">
<div class="traffic-lights" aria-hidden="true">
<span></span><span></span><span></span>
</div>
<div class="address"><span class="lock"></span> files.example.com/releases/</div>
<span class="browser-menu" aria-hidden="true">•••</span>
</div>
<div class="file-header">
<span>Index of /releases/</span>
<div>
<button type="button">Copy Links</button>
<button type="button">Download Playlist</button>
</div>
</div>
<div class="file-list">
<a class="parent" href="#preview"><span class="file-icon"></span>../</a>
<a class="folder" href="#preview"><span class="file-icon"></span>archive/</a>
<a class="folder" href="#preview"><span class="file-icon"></span>latest/</a>
<a href="#preview"><span class="file-icon"></span>nais-v1.2.0.zip</a>
<a href="#preview"><span class="file-icon"></span>checksums.txt</a>
<a href="#preview"><span class="file-icon"></span>release-notes.pdf</a>
<p>— nginx autoindex —</p>
</div>
</div>
<div class="status-pill"><span></span> Lightweight. No framework.</div>
</div>
</section>
<section class="metrics" aria-label="Project highlights">
<div><strong>3</strong><span>small assets</span></div>
<div><strong>1</strong><span>install command</span></div>
<div><strong>0</strong><span>runtime dependencies</span></div>
<div><strong>100%</strong><span>nginx native</span></div>
</section>
<section class="section" id="features">
<div class="section-heading">
<span class="section-kicker">Why NAIS</span>
<h2>A directory listing people can actually use.</h2>
<p>Small additions that make browsing and sharing files noticeably better.</p>
</div>
<div class="feature-grid">
<article class="feature-card feature-card-wide">
<span class="feature-number">01</span>
<div class="feature-visual rows-visual" aria-hidden="true">
<span class="pink"></span><span class="green"></span><span class="green short"></span><span class="blue"></span>
</div>
<h3>Clear at a glance</h3>
<p>Folders, parent navigation, and files use distinct colors so every directory is easy to scan.</p>
</article>
<article class="feature-card">
<span class="feature-number">02</span>
<div class="feature-icon link-icon" aria-hidden="true"></div>
<h3>Copy every link</h3>
<p>Collect all file URLs from the current directory with one click.</p>
</article>
<article class="feature-card">
<span class="feature-number">03</span>
<div class="feature-icon play-icon" aria-hidden="true"></div>
<h3>Build playlists</h3>
<p>Export media links as a ready-to-open playlist directly from the listing.</p>
</article>
<article class="feature-card feature-card-wide install-card">
<span class="feature-number">04</span>
<div class="terminal-mini" aria-hidden="true">
<div><i></i><i></i><i></i></div>
<code><b>$</b> curl ... | sh</code>
<code><b></b> NAIS configured successfully.</code>
</div>
<h3>Setup that respects nginx</h3>
<p>The installer detects common layouts, validates with <code>nginx -t</code>, and restores prior configuration if anything fails.</p>
</article>
</div>
</section>
<section class="section install-section" id="install">
<div class="install-copy">
<span class="section-kicker">Get started</span>
<h2>From default to polished in one command.</h2>
<p>
Run the installer, answer four short prompts, and NAIS handles
the assets, nginx configuration, validation, and reload.
</p>
<ul class="check-list">
<li>HTTP or optional HTTPS configuration</li>
<li>Automatic nginx include detection</li>
<li>Safe validation and rollback</li>
</ul>
</div>
<div class="terminal">
<div class="terminal-bar">
<div aria-hidden="true"><span></span><span></span><span></span></div>
<p>install nais</p>
<button type="button" data-copy-command><span class="copy-label">Copy</span></button>
</div>
<pre><span class="prompt">$</span> curl -fsSL https://meghdadfadaee.github.io/nais/setup.sh | sh
<span class="muted">Server name:</span> files.example.com
<span class="muted">Root path:</span> /srv/files
<span class="muted">Assets path:</span> /var/www/nais-assets
<span class="muted">Certificates path (optional):</span>
<span class="success"></span> Downloading NAIS assets...
<span class="success"></span> Validating nginx configuration...
<span class="success"></span> NAIS configured successfully.</pre>
</div>
</section>
<section class="cta">
<img src="./favicon.png" alt="" width="56" height="56">
<h2>Make the simple thing look good.</h2>
<p>A focused autoindex theme, built from plain CSS and JavaScript.</p>
<a class="button button-primary" href="https://github.com/meghdadfadaee/nais">Explore on GitHub</a>
</section>
</main>
<footer>
<a class="brand footer-brand" href="./">
<img src="./favicon.png" alt="" width="34" height="34">
<span>NAIS</span>
</a>
<p>Nginx Autoindex Skin</p>
<a href="https://github.com/meghdadfadaee/nais">GitHub</a>
</footer>
</body>
</html>

978
landing.css Normal file
View File

@@ -0,0 +1,978 @@
:root {
--bg: #0f111a;
--bg-deep: #0a0c13;
--surface: #151926;
--surface-bright: #1a1f30;
--row-hover: #1f2440;
--text: #e6e6eb;
--muted: #8b93a7;
--accent: #7aa2f7;
--folder: #9ece6a;
--parent: #f7768e;
--brand: #f7d759;
--border: #2a2f4a;
--page-width: 1180px;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
overflow-x: hidden;
background: var(--bg);
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.6;
}
button,
a {
-webkit-tap-highlight-color: transparent;
}
a {
color: inherit;
}
.ambient {
position: absolute;
z-index: -1;
width: 500px;
height: 500px;
border-radius: 50%;
filter: blur(130px);
opacity: .14;
pointer-events: none;
}
.ambient-one {
top: -220px;
right: -120px;
background: var(--accent);
}
.ambient-two {
top: 550px;
left: -350px;
background: var(--folder);
}
.site-header,
main,
footer {
width: min(calc(100% - 40px), var(--page-width));
margin-inline: auto;
}
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 84px;
border-bottom: 1px solid rgba(42, 47, 74, .7);
}
.brand {
display: inline-flex;
align-items: center;
gap: 11px;
color: var(--text);
font-size: 18px;
font-weight: 800;
letter-spacing: .12em;
text-decoration: none;
}
.brand img {
width: 42px;
height: 42px;
}
.site-nav {
display: flex;
align-items: center;
gap: 30px;
color: var(--muted);
font-size: 14px;
font-weight: 600;
}
.site-nav a {
text-decoration: none;
transition: color .2s ease;
}
.site-nav a:hover {
color: var(--text);
}
.site-nav .github-link {
padding: 8px 14px;
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
}
.hero {
display: grid;
grid-template-columns: minmax(0, .88fr) minmax(520px, 1.12fr);
align-items: center;
gap: 70px;
min-height: 720px;
padding: 74px 0 92px;
}
.eyebrow,
.section-kicker {
color: var(--accent);
font-size: 12px;
font-weight: 800;
letter-spacing: .16em;
text-transform: uppercase;
}
.eyebrow {
display: flex;
align-items: center;
gap: 10px;
}
.eyebrow span {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--folder);
box-shadow: 0 0 0 5px rgba(158, 206, 106, .1), 0 0 18px rgba(158, 206, 106, .55);
}
.hero h1 {
margin: 22px 0 22px;
font-size: clamp(54px, 6vw, 84px);
line-height: .98;
letter-spacing: -.065em;
}
.hero h1 strong {
color: var(--accent);
font-weight: 800;
}
.hero-text {
max-width: 560px;
margin: 0;
color: var(--muted);
font-size: 18px;
}
.hero-actions {
display: flex;
gap: 12px;
margin-top: 34px;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 46px;
padding: 0 20px;
border: 1px solid transparent;
border-radius: 9px;
font-size: 14px;
font-weight: 750;
text-decoration: none;
transition: transform .2s ease, background .2s ease, border-color .2s ease;
}
.button:hover {
transform: translateY(-2px);
}
.button-primary {
background: var(--accent);
color: #0c1020;
}
.button-primary:hover {
background: #96b8ff;
}
.button-secondary {
border-color: var(--border);
background: rgba(21, 25, 38, .7);
color: var(--text);
}
.button-secondary:hover {
border-color: var(--accent);
background: var(--surface-bright);
}
.quick-command {
display: flex;
align-items: center;
gap: 10px;
max-width: 560px;
margin-top: 22px;
padding: 10px 10px 10px 14px;
border: 1px solid var(--border);
border-radius: 10px;
background: rgba(10, 12, 19, .65);
}
.quick-command code {
min-width: 0;
overflow: hidden;
color: var(--muted);
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.copy-button,
.terminal-bar button {
flex: 0 0 auto;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
color: var(--text);
cursor: pointer;
font: inherit;
font-size: 11px;
font-weight: 700;
}
.copy-button {
padding: 6px 9px;
}
.copy-button:hover,
.terminal-bar button:hover {
border-color: var(--accent);
}
.preview-wrap {
position: relative;
perspective: 1200px;
}
.preview-glow {
position: absolute;
inset: 12% 5% -2%;
z-index: -1;
border-radius: 50%;
background: rgba(122, 162, 247, .28);
filter: blur(65px);
}
.browser {
overflow: hidden;
border: 1px solid #343b5b;
border-radius: 14px;
background: var(--bg-deep);
box-shadow: 0 35px 80px rgba(0, 0, 0, .38);
transform: rotateY(-4deg) rotateX(1deg);
animation: enter .8s ease-out both;
}
.browser-bar {
display: grid;
grid-template-columns: 80px 1fr 80px;
align-items: center;
min-height: 48px;
padding: 0 14px;
border-bottom: 1px solid var(--border);
background: #121520;
}
.traffic-lights {
display: flex;
gap: 6px;
}
.traffic-lights span,
.terminal-bar > div span {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--parent);
}
.traffic-lights span:nth-child(2),
.terminal-bar > div span:nth-child(2) {
background: var(--brand);
}
.traffic-lights span:nth-child(3),
.terminal-bar > div span:nth-child(3) {
background: var(--folder);
}
.address {
overflow: hidden;
padding: 5px 12px;
border: 1px solid #252a3f;
border-radius: 6px;
background: #0c0f17;
color: #737b91;
font: 11px ui-monospace, SFMono-Regular, Menlo, monospace;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.lock {
color: var(--folder);
}
.browser-menu {
justify-self: end;
color: var(--muted);
font-size: 12px;
letter-spacing: 2px;
}
.file-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-height: 68px;
padding: 0 22px;
border-bottom: 1px solid var(--border);
background: var(--surface);
font-size: 14px;
font-weight: 650;
}
.file-header div {
display: flex;
gap: 7px;
}
.file-header button {
padding: 6px 8px;
border: 1px solid var(--border);
border-radius: 6px;
background: transparent;
color: var(--text);
font-size: 9px;
}
.file-list {
padding: 10px 0 20px;
}
.file-list a {
display: flex;
align-items: center;
gap: 13px;
padding: 9px 22px;
color: var(--accent);
font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
text-decoration: none;
transition: background .15s ease, color .15s ease;
}
.file-list a:hover {
background: var(--row-hover);
color: #fff;
}
.file-list .folder {
color: var(--folder);
font-weight: 700;
}
.file-list .parent {
color: var(--parent);
font-weight: 700;
}
.file-icon {
width: 9px;
height: 9px;
border-radius: 2px;
background: currentColor;
opacity: .9;
}
.file-list p {
margin: 17px 22px 0;
color: var(--muted);
font: 9px ui-monospace, SFMono-Regular, Menlo, monospace;
}
.status-pill {
position: absolute;
right: -18px;
bottom: -25px;
display: flex;
align-items: center;
gap: 8px;
padding: 9px 13px;
border: 1px solid var(--border);
border-radius: 999px;
background: rgba(21, 25, 38, .92);
box-shadow: 0 15px 35px rgba(0, 0, 0, .3);
color: var(--muted);
font-size: 11px;
font-weight: 700;
backdrop-filter: blur(10px);
}
.status-pill span {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--folder);
box-shadow: 0 0 12px var(--folder);
}
.metrics {
display: grid;
grid-template-columns: repeat(4, 1fr);
border-block: 1px solid var(--border);
}
.metrics div {
display: flex;
flex-direction: column;
gap: 2px;
padding: 28px;
border-right: 1px solid var(--border);
}
.metrics div:last-child {
border-right: 0;
}
.metrics strong {
color: var(--text);
font-size: 24px;
}
.metrics span {
color: var(--muted);
font-size: 11px;
font-weight: 700;
letter-spacing: .1em;
text-transform: uppercase;
}
.section {
padding: 130px 0 0;
}
.section-heading {
max-width: 650px;
margin-bottom: 44px;
}
.section h2,
.cta h2 {
margin: 12px 0 15px;
font-size: clamp(36px, 5vw, 58px);
line-height: 1.08;
letter-spacing: -.05em;
}
.section-heading p,
.install-copy > p,
.cta p {
margin: 0;
color: var(--muted);
font-size: 17px;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.feature-card {
position: relative;
min-height: 300px;
overflow: hidden;
padding: 32px;
border: 1px solid var(--border);
border-radius: 14px;
background: linear-gradient(145deg, rgba(26, 31, 48, .9), rgba(17, 20, 31, .9));
transition: border-color .25s ease, transform .25s ease;
}
.feature-card:hover {
border-color: #424b72;
transform: translateY(-3px);
}
.feature-card-wide {
grid-row: span 2;
min-height: 616px;
}
.feature-number {
color: #626b83;
font: 11px ui-monospace, SFMono-Regular, Menlo, monospace;
}
.feature-card h3 {
position: absolute;
bottom: 72px;
margin: 0;
font-size: 22px;
letter-spacing: -.025em;
}
.feature-card p {
position: absolute;
right: 32px;
bottom: 27px;
left: 32px;
margin: 0;
color: var(--muted);
font-size: 13px;
}
.feature-visual {
position: absolute;
inset: 85px 45px auto;
display: grid;
gap: 14px;
padding: 30px;
border: 1px solid var(--border);
border-radius: 12px;
background: #0d1018;
transform: rotate(-3deg);
}
.rows-visual span {
width: 78%;
height: 10px;
border-radius: 3px;
background: var(--accent);
box-shadow: -18px 0 0 -3px currentColor;
}
.rows-visual .pink {
width: 42%;
background: var(--parent);
color: var(--parent);
}
.rows-visual .green {
width: 65%;
background: var(--folder);
color: var(--folder);
}
.rows-visual .short {
width: 48%;
}
.rows-visual .blue {
color: var(--accent);
}
.feature-icon {
position: absolute;
top: 76px;
left: 32px;
width: 62px;
height: 62px;
border: 1px solid var(--border);
border-radius: 16px;
background: var(--bg-deep);
box-shadow: 0 15px 35px rgba(0, 0, 0, .25);
}
.link-icon::before,
.link-icon::after {
position: absolute;
width: 25px;
height: 10px;
border: 3px solid var(--accent);
border-radius: 10px;
content: "";
transform: rotate(-35deg);
}
.link-icon::before {
top: 18px;
left: 12px;
}
.link-icon::after {
right: 11px;
bottom: 17px;
}
.play-icon::before {
position: absolute;
top: 19px;
left: 23px;
border-top: 12px solid transparent;
border-bottom: 12px solid transparent;
border-left: 18px solid var(--folder);
content: "";
}
.install-card {
grid-column: 1 / -1;
grid-row: auto;
min-height: 360px;
}
.terminal-mini {
position: absolute;
top: 78px;
right: 32px;
left: 32px;
display: grid;
gap: 16px;
padding: 20px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg-deep);
font: 11px ui-monospace, SFMono-Regular, Menlo, monospace;
}
.terminal-mini div {
display: flex;
gap: 5px;
}
.terminal-mini i {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--parent);
}
.terminal-mini i:nth-child(2) {
background: var(--brand);
}
.terminal-mini i:nth-child(3),
.terminal-mini code:last-child b {
background: var(--folder);
color: var(--folder);
}
.terminal-mini b {
color: var(--accent);
}
.install-section {
display: grid;
grid-template-columns: .8fr 1.2fr;
align-items: center;
gap: 90px;
}
.check-list {
display: grid;
gap: 12px;
margin: 28px 0 0;
padding: 0;
color: var(--text);
font-size: 13px;
list-style: none;
}
.check-list li::before {
margin-right: 10px;
color: var(--folder);
content: "✓";
}
.terminal {
overflow: hidden;
border: 1px solid #343b5b;
border-radius: 14px;
background: var(--bg-deep);
box-shadow: 0 30px 70px rgba(0, 0, 0, .3);
}
.terminal-bar {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: 11px 14px;
border-bottom: 1px solid var(--border);
background: #121520;
}
.terminal-bar > div {
display: flex;
gap: 6px;
}
.terminal-bar p {
margin: 0;
color: var(--muted);
font-size: 10px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
}
.terminal-bar button {
justify-self: end;
padding: 5px 9px;
}
.terminal pre {
overflow-x: auto;
margin: 0;
padding: 30px;
color: var(--text);
font: 12px/2 ui-monospace, SFMono-Regular, Menlo, monospace;
}
.terminal .prompt {
color: var(--accent);
}
.terminal .muted {
color: var(--muted);
}
.terminal .success {
color: var(--folder);
}
.cta {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 130px;
padding: 90px 20px;
border: 1px solid var(--border);
border-radius: 16px;
background:
radial-gradient(circle at 50% 0%, rgba(122, 162, 247, .18), transparent 48%),
var(--surface);
text-align: center;
}
.cta h2 {
margin-top: 24px;
}
.cta .button {
margin-top: 30px;
}
footer {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
margin-top: 48px;
padding: 28px 0 36px;
border-top: 1px solid var(--border);
color: var(--muted);
font-size: 12px;
}
.footer-brand {
font-size: 14px;
}
.footer-brand img {
width: 34px;
height: 34px;
}
footer p {
margin: 0;
}
footer > a:last-child {
justify-self: end;
text-decoration: none;
}
footer > a:last-child:hover {
color: var(--text);
}
@keyframes enter {
from {
opacity: 0;
transform: translateY(18px) rotateY(-4deg) rotateX(1deg);
}
to {
opacity: 1;
transform: translateY(0) rotateY(-4deg) rotateX(1deg);
}
}
@media (max-width: 980px) {
.hero {
grid-template-columns: 1fr;
gap: 70px;
padding-top: 80px;
}
.hero-copy {
max-width: 650px;
}
.preview-wrap {
max-width: 700px;
margin-inline: auto;
}
.install-section {
grid-template-columns: 1fr;
gap: 45px;
}
}
@media (max-width: 700px) {
.site-header,
main,
footer {
width: min(calc(100% - 28px), var(--page-width));
}
.site-header {
min-height: 72px;
}
.site-nav {
gap: 14px;
}
.site-nav a:not(.github-link) {
display: none;
}
.hero {
min-height: auto;
padding: 65px 0 80px;
}
.hero h1 {
font-size: clamp(48px, 15vw, 68px);
}
.hero-text {
font-size: 16px;
}
.hero-actions {
flex-wrap: wrap;
}
.quick-command {
display: none;
}
.browser {
transform: none;
}
.browser-bar {
grid-template-columns: 58px 1fr 24px;
}
.file-header {
align-items: flex-start;
flex-direction: column;
padding-block: 15px;
}
.file-list a {
padding-inline: 16px;
}
.status-pill {
right: 8px;
}
.metrics {
grid-template-columns: repeat(2, 1fr);
}
.metrics div {
padding: 20px;
border-bottom: 1px solid var(--border);
}
.metrics div:nth-child(2) {
border-right: 0;
}
.metrics div:nth-child(n+3) {
border-bottom: 0;
}
.section {
padding-top: 95px;
}
.feature-grid {
grid-template-columns: 1fr;
}
.feature-card,
.feature-card-wide,
.install-card {
grid-column: auto;
grid-row: auto;
min-height: 330px;
}
.feature-card-wide {
min-height: 450px;
}
.install-card {
min-height: 390px;
}
.install-card h3 {
bottom: 115px;
}
.terminal pre {
padding: 22px;
font-size: 10px;
}
.cta {
margin-top: 95px;
padding-block: 65px;
}
footer {
grid-template-columns: 1fr auto;
}
footer p {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
}
}

16
landing.js Normal file
View File

@@ -0,0 +1,16 @@
const installCommand = 'curl -fsSL https://meghdadfadaee.github.io/nais/setup.sh | sh';
document.querySelectorAll('[data-copy-command]').forEach((button) => {
button.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(installCommand);
const label = button.querySelector('.copy-label');
label.textContent = 'Copied';
window.setTimeout(() => {
label.textContent = 'Copy';
}, 1800);
} catch {
window.prompt('Copy the install command:', installCommand);
}
});
});

332
setup.sh Executable file
View File

@@ -0,0 +1,332 @@
#!/bin/sh
set -eu
BASE_URL=${NAIS_BASE_URL:-https://meghdadfadaee.github.io/nais}
DEFAULT_ASSETS_PATH=/var/www/nais-assets
MANAGED_INCLUDE='include nais-sites/*.conf;'
say() {
printf '%s\n' "$*"
}
fail() {
printf 'nais setup: %s\n' "$*" >&2
exit 1
}
need_command() {
command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1"
}
prompt() {
prompt_text=$1
default_value=${2-}
if [ -n "$default_value" ]; then
printf '%s [%s]: ' "$prompt_text" "$default_value" >/dev/tty
else
printf '%s: ' "$prompt_text" >/dev/tty
fi
IFS= read -r answer </dev/tty || fail "could not read input from /dev/tty"
if [ -z "$answer" ]; then
answer=$default_value
fi
printf '%s' "$answer"
}
validate_absolute_path() {
value=$1
label=$2
case "$value" in
/*) ;;
*) fail "$label must be an absolute path" ;;
esac
case "$value" in
*'
'*) fail "$label must not contain newlines" ;;
esac
}
nginx_quote() {
value=$1
case "$value" in
*'"'*|*'\'*|*'$'*) fail "paths containing quotes, backslashes, or dollar signs are not supported" ;;
esac
printf '"%s"' "$value"
}
run_root() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
else
sudo "$@"
fi
}
cleanup() {
status=$?
trap - EXIT HUP INT TERM
if [ "$status" -ne 0 ] && [ "${TRANSACTION_ACTIVE:-0}" -eq 1 ]; then
restore_previous
fi
if [ -n "${WORK_DIR:-}" ]; then
rm -rf "$WORK_DIR"
fi
exit "$status"
}
restore_previous() {
say "Setup failed; restoring the previous nginx configuration and assets."
set +e
if [ "$SITE_EXISTED" -eq 1 ]; then
run_root cp "$SITE_BACKUP" "$SITE_CONFIG"
else
run_root rm -f "$SITE_CONFIG"
fi
if [ "$MAIN_CHANGED" -eq 1 ]; then
run_root cp "$MAIN_BACKUP" "$NGINX_CONF"
fi
for asset in autoindex.css autoindex.js favicon.ico; do
if [ -f "$WORK_DIR/assets-backup/$asset" ]; then
run_root cp "$WORK_DIR/assets-backup/$asset" "$ASSETS_PATH/$asset"
else
run_root rm -f "$ASSETS_PATH/$asset"
fi
done
if ! run_root "$NGINX_BIN" -t; then
printf '%s\n' "nais setup: rollback completed, but the restored nginx configuration is invalid" >&2
fi
}
trap cleanup EXIT
trap 'exit 1' HUP INT TERM
if ! ( : </dev/tty && : >/dev/tty ) 2>/dev/null; then
fail "/dev/tty is required for interactive setup"
fi
need_command curl
need_command nginx
need_command awk
need_command sed
need_command mktemp
if [ "$(id -u)" -ne 0 ]; then
need_command sudo
sudo -v || fail "root privileges are required to configure nginx"
fi
SERVER_NAME=$(prompt "Server name")
[ -n "$SERVER_NAME" ] || fail "server_name is required"
case "$SERVER_NAME" in
*[!A-Za-z0-9._:-]*) fail "server_name must be one hostname or IP address without whitespace" ;;
esac
ROOT_PATH=$(prompt "Root path")
[ -n "$ROOT_PATH" ] || fail "root_path is required"
validate_absolute_path "$ROOT_PATH" "root_path"
ASSETS_PATH=$(prompt "Assets path" "$DEFAULT_ASSETS_PATH")
validate_absolute_path "$ASSETS_PATH" "assets_path"
CERTIFICATES_PATH=$(prompt "Certificates path (optional)")
if [ -n "$CERTIFICATES_PATH" ]; then
validate_absolute_path "$CERTIFICATES_PATH" "certificates_path"
run_root test -f "$CERTIFICATES_PATH/fullchain.pem" ||
fail "certificate not found: $CERTIFICATES_PATH/fullchain.pem"
run_root test -f "$CERTIFICATES_PATH/privkey.pem" ||
fail "certificate key not found: $CERTIFICATES_PATH/privkey.pem"
fi
NGINX_BIN=$(command -v nginx)
NGINX_VERSION=$("$NGINX_BIN" -V 2>&1)
NGINX_PREFIX=$(printf '%s\n' "$NGINX_VERSION" | sed -n 's/.*--prefix=\([^ ]*\).*/\1/p')
NGINX_CONF=$(printf '%s\n' "$NGINX_VERSION" | sed -n 's/.*--conf-path=\([^ ]*\).*/\1/p')
[ -n "$NGINX_PREFIX" ] || NGINX_PREFIX=/etc/nginx
[ -n "$NGINX_CONF" ] || NGINX_CONF=$NGINX_PREFIX/conf/nginx.conf
case "$NGINX_CONF" in
/*) ;;
*) NGINX_CONF=$NGINX_PREFIX/$NGINX_CONF ;;
esac
run_root test -f "$NGINX_CONF" || fail "nginx configuration not found: $NGINX_CONF"
WORK_DIR=$(mktemp -d)
SITE_BACKUP=$WORK_DIR/site.conf.backup
MAIN_BACKUP=$WORK_DIR/nginx.conf.backup
GENERATED_CONFIG=$WORK_DIR/nais.conf
MAIN_CURRENT=$WORK_DIR/nginx.conf.current
MAIN_CHANGED=0
MAIN_NEEDS_CHANGE=0
SITE_EXISTED=0
TRANSACTION_ACTIVE=0
run_root cp "$NGINX_CONF" "$MAIN_CURRENT"
run_root chmod a+r "$MAIN_CURRENT"
say "Downloading NAIS assets..."
for asset in autoindex.css autoindex.js favicon.ico; do
curl -fsSL "$BASE_URL/$asset" -o "$WORK_DIR/$asset" ||
fail "failed to download $BASE_URL/$asset"
done
ROOT_QUOTED=$(nginx_quote "$ROOT_PATH")
ASSETS_QUOTED=$(nginx_quote "$ASSETS_PATH")
if [ -n "$CERTIFICATES_PATH" ]; then
CERT_QUOTED=$(nginx_quote "$CERTIFICATES_PATH/fullchain.pem")
KEY_QUOTED=$(nginx_quote "$CERTIFICATES_PATH/privkey.pem")
cat >"$GENERATED_CONFIG" <<EOF
# Managed by NAIS setup.sh. Re-running setup replaces this file.
server {
listen 443 ssl;
server_name $SERVER_NAME;
ssl_certificate $CERT_QUOTED;
ssl_certificate_key $KEY_QUOTED;
root $ROOT_QUOTED;
location / {
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
sub_filter '</head>' '<link rel="stylesheet" href="/autoindex.css">';
sub_filter '</body>' '<script src="/autoindex.js"></script></body>';
sub_filter_once on;
}
location = /autoindex.css { root $ASSETS_QUOTED; }
location = /autoindex.js { root $ASSETS_QUOTED; }
location = /favicon.ico { root $ASSETS_QUOTED; }
}
server {
listen 80;
server_name $SERVER_NAME;
return 301 https://$SERVER_NAME\$request_uri;
}
EOF
else
cat >"$GENERATED_CONFIG" <<EOF
# Managed by NAIS setup.sh. Re-running setup replaces this file.
server {
listen 80;
server_name $SERVER_NAME;
root $ROOT_QUOTED;
location / {
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
sub_filter '</head>' '<link rel="stylesheet" href="/autoindex.css">';
sub_filter '</body>' '<script src="/autoindex.js"></script></body>';
sub_filter_once on;
}
location = /autoindex.css { root $ASSETS_QUOTED; }
location = /autoindex.js { root $ASSETS_QUOTED; }
location = /favicon.ico { root $ASSETS_QUOTED; }
}
EOF
fi
ACTIVE_INCLUDE=$(
sed 's/[[:space:]]*#.*$//' "$MAIN_CURRENT" |
awk '
$1 == "include" {
path = $2
sub(/;$/, "", path)
gsub(/^"|"$/, "", path)
if (path ~ /sites-enabled\/\*$/) { print path; exit }
if (path ~ /conf\.d\/\*\.conf$/) { print path; exit }
if (path ~ /http\.d\/\*\.conf$/) { print path; exit }
if (path ~ /nais-sites\/\*\.conf$/) { print path; exit }
}
'
)
if [ -n "$ACTIVE_INCLUDE" ]; then
case "$ACTIVE_INCLUDE" in
/*) INCLUDE_PATTERN=$ACTIVE_INCLUDE ;;
*) INCLUDE_PATTERN=$NGINX_PREFIX/$ACTIVE_INCLUDE ;;
esac
SITE_DIR=${INCLUDE_PATTERN%/*}
else
SITE_DIR=$NGINX_PREFIX/nais-sites
cp "$MAIN_CURRENT" "$MAIN_BACKUP"
awk -v include_line=" $MANAGED_INCLUDE" '
BEGIN { added = 0 }
!added && $0 ~ /^[[:space:]]*http[[:space:]]*\{/ {
print
print include_line
added = 1
next
}
{ print }
END {
if (!added) exit 42
}
' "$MAIN_CURRENT" >"$WORK_DIR/nginx.conf" ||
fail "could not find the nginx http block in $NGINX_CONF"
MAIN_NEEDS_CHANGE=1
fi
SITE_CONFIG=$SITE_DIR/nais.conf
run_root mkdir -p "$ROOT_PATH" "$ASSETS_PATH" "$SITE_DIR"
mkdir -p "$WORK_DIR/assets-backup"
for asset in autoindex.css autoindex.js favicon.ico; do
if run_root test -e "$ASSETS_PATH/$asset" && ! run_root test -f "$ASSETS_PATH/$asset"; then
fail "asset destination is not a regular file: $ASSETS_PATH/$asset"
fi
if run_root test -f "$ASSETS_PATH/$asset"; then
run_root cp "$ASSETS_PATH/$asset" "$WORK_DIR/assets-backup/$asset"
fi
done
if run_root test -e "$SITE_CONFIG"; then
run_root test -f "$SITE_CONFIG" || fail "site config destination is not a regular file: $SITE_CONFIG"
run_root cp "$SITE_CONFIG" "$SITE_BACKUP"
SITE_EXISTED=1
fi
TRANSACTION_ACTIVE=1
run_root chmod a+rx "$ASSETS_PATH"
for asset in autoindex.css autoindex.js favicon.ico; do
run_root cp "$WORK_DIR/$asset" "$ASSETS_PATH/$asset"
run_root chmod a+r "$ASSETS_PATH/$asset"
done
run_root cp "$GENERATED_CONFIG" "$SITE_CONFIG"
if [ "$MAIN_NEEDS_CHANGE" -eq 1 ]; then
MAIN_CHANGED=1
run_root cp "$WORK_DIR/nginx.conf" "$NGINX_CONF"
fi
say "Validating nginx configuration..."
if ! run_root "$NGINX_BIN" -t; then
fail "nginx validation failed"
fi
if command -v systemctl >/dev/null 2>&1; then
if run_root systemctl is-active --quiet nginx; then
run_root systemctl reload nginx
else
run_root systemctl enable --now nginx
fi
elif command -v service >/dev/null 2>&1; then
run_root service nginx restart
else
fail "nginx configuration is installed, but no supported service manager was found"
fi
TRANSACTION_ACTIVE=0
say "NAIS configured successfully."
say "Site config: $SITE_CONFIG"
say "Assets: $ASSETS_PATH"