Nginx Autoindex 样式美化
0x01 前言
nginx开启autoindex可以支持目录访问,但是原生的太丑了,通过修改 nginx.conf 配置文件中的 add_after_body 配置项引入一个html,通过 js 将原生界面的 dom 元素提取出来,然后重新布局添加样式,来实现美化。
1、支持面包屑导航。
2、格式化输出样式。
3、可以对文件名,修改时间,大小排序。

0x02 实现
1、首先添加 add_after_body 字段。
location / {
root /usr/share/nginx/html;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
add_after_body /.autoindex.html;
charset utf-8;
}
2、把.autoindex.html放到站点目录下。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件列表</title>
</head>
<body>
<style>
body > hr{display:none}
.yq-row{display:flex;justify-content:flex-start;align-items:center;margin:0;padding:0}
.yq-row > *{flex:1}
.yq-row > a{flex:1 1 auto;min-width:0;max-width:none}
.yq-row .last-modified,.yq-row .size{flex:0 0 auto;min-width:0;padding:0 10px;text-align:right;white-space:nowrap}
.yq-row .last-modified{padding-right:5px;width:150px;flex-shrink:0}
.yq-row .size{padding-left:5px;width:90px;flex-shrink:0}
.yq-header{display:flex;justify-content:flex-start;align-items:center;margin:0;padding:8px 5px;font-weight:bold;border-bottom:2px solid #ccc;background-color:#f5f5f5}
.yq-header > *{flex:1}
.yq-header .header-name{flex:1 1 auto;min-width:0}
.yq-header .header-date,.yq-header .header-size{flex:0 0 auto;padding:0 10px;text-align:right;white-space:nowrap}
.yq-header .header-date{padding-right:5px;width:150px;flex-shrink:0}
.yq-header .header-size{padding-left:5px;width:90px;flex-shrink:0}
h1.yq-heading-breadcrumb{font-size:22px;font-weight:bold;color:#333}
h1.yq-heading-breadcrumb a{color:#0070c9;text-decoration:none}
h1.yq-heading-breadcrumb a:hover{text-decoration:underline}
h1.yq-heading-breadcrumb span{color:#333}
.yq-footer{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);font-size:12px;color:#666;text-align:center;z-index:999;pointer-events:none}
.yq-header > div{cursor:pointer;user-select:none;position:relative;padding-right:20px}
.yq-header > div:hover{background-color:#e0e0e0}
.yq-header > div::after{content:'';position:absolute;right:5px;top:50%;transform:translateY(-50%);width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;opacity:0.3}
.yq-header > div.sort-asc::after{border-bottom:6px solid #333;border-top:none;opacity:1}
.yq-header > div.sort-desc::after{border-top:6px solid #333;border-bottom:none;opacity:1}
.yq-outer-box a{color:#000;padding:4px 5px;margin:0 -5px;white-space:nowrap;overflow:hidden;display:block;text-overflow:ellipsis;text-decoration:none;flex:1 1 0;min-width:0}
.yq-outer-box a::before{display:inline-block;vertical-align:middle;margin-right:10px;width:24px;text-align:center;line-height:12px}
.yq-outer-box a.file::before{content:url("data:image/svg+xml;utf8,<svg width='15' height='19' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M10 8C8.34 8 7 6.66 7 5V1H3c-1.1 0-2 .9-2 2v13c0 1.1.9 2 2 2h9c1.1 0 2-.9 2-2V8h-4zM8 5c0 1.1.9 2 2 2h3.59L8 1.41V5zM3 0h5l7 7v9c0 1.66-1.34 3-3 3H3c-1.66 0-3-1.34-3-3V3c0-1.66 1.34-3 3-3z' fill='black'/></svg>")}
.yq-outer-box a:hover{text-decoration:underline}
.yq-outer-box a.folder::before{content:url("data:image/svg+xml;utf8,<svg width='20' height='19' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M18.784 3.87a1.565 1.565 0 0 0-.565-.356V2.426c0-.648-.523-1.171-1.15-1.171H8.996L7.908.25A.89.89 0 0 0 7.302 0H2.094C1.445 0 .944.523.944 1.171v2.3c-.21.085-.398.21-.565.356a1.348 1.348 0 0 0-.377 1.004l.398 9.83C.42 15.393 1.048 16 1.8 16h15.583c.753 0 1.36-.586 1.4-1.339l.398-9.83c.021-.313-.125-.69-.397-.962zM1.843 3.41V1.191c0-.146.104-.272.25-.272H7.26l1.234 1.088c.083.042.167.104.293.104h8.282c.125 0 .25.126.25.272V3.41H1.844zm15.54 11.712H1.78a.47.47 0 0 1-.481-.46l-.397-9.83c0-.147.041-.252.125-.356a.504.504 0 0 1 .377-.147H17.78c.125 0 .272.063.377.147.083.083.125.209.125.334l-.418 9.83c-.021.272-.23.482-.481.482z' fill='black'/></svg>")}
.yq-outer-box a.lambda::before{content:url("data:image/svg+xml;utf8,<svg width='15' height='19' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M3.5 14.4354H5.31622L7.30541 9.81311H7.43514L8.65315 13.0797C9.05676 14.1643 9.55405 14.5 10.7 14.5C11.0171 14.5 11.291 14.4677 11.5 14.4032V13.1572C11.3847 13.1766 11.2622 13.2024 11.1541 13.2024C10.6351 13.2024 10.3829 13.0281 10.1595 12.4664L8.02613 7.07586C7.21171 5.01646 6.54865 4.5 5.11441 4.5C4.83333 4.5 4.62432 4.53228 4.37207 4.59038V5.83635C4.56667 5.81052 4.66036 5.79761 4.77568 5.79761C5.64775 5.79761 5.9 6.0042 6.4045 7.19852L6.64234 7.77954L3.5 14.4354Z' fill='black'/><rect x='0.5' y='0.5' width='14' height='18' rx='2.5' stroke='black'/></svg>")}
.yq-outer-box a.img::before{content:url("data:image/svg+xml;utf8,<svg width='16' height='19' viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg' fill='none' stroke='black' stroke-width='5' stroke-linecap='round' stroke-linejoin='round'><rect x='6' y='6' width='68' height='68' rx='5' ry='5'/><circle cx='24' cy='24' r='8'/><path d='M73 49L59 34 37 52m16 20L27 42 7 58'/></svg>")}
</style>
<script>
function getClassName(filename) {
if (!filename) {
return 'file';
}
if (filename.endsWith('/')) {
return 'folder';
}
const array = filename.split('.');
let suffix = array[array.length - 1];
if (!suffix) {
return 'file';
}
suffix = suffix.toLowerCase();
const img = ['gif', 'jpg', 'png', 'svg', 'jpeg', 'bmp'];
if (img.includes(suffix)) {
return 'img';
}
const lambda = ['java', 'js', 'ts', 'go', 'c', 'cpp', 'cs', 'py', 'sh', 'swift', 'php', 'html', 'css', 'xml', 'json', 'yml', 'yaml', 'md', 'log', 'ini', 'conf', 'properties', 'cmd', 'bat'];
if (lambda.includes(suffix)) {
return 'lambda';
}
return 'file';
}
function formatDate(date, time) {
const mon = {
Jan: '01',
Feb: '02',
Mar: '03',
Apr: '04',
May: '05',
Jun: '06',
Jul: '07',
Aug: '08',
Sep: '09',
Oct: '10',
Nov: '11',
Dec: '12',
};
const [day, month, year] = date.split('-');
return `${year}-${mon[month]}-${day} ${time}`;
}
function formatSize(size) {
if (size === '-' || !size || size.trim() === '') {
return '';
}
// 移除可能的逗号分隔符
const cleanSize = size.toString().replace(/,/g, '');
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let s = Number(cleanSize);
if (isNaN(s) || s < 0) {
// 如果无法解析,返回原始值(可能是已经格式化的字符串)
return size;
}
for (const u of units) {
if (s < 1024) {
return s.toFixed(2) + ' ' + u;
}
s = s / 1024;
}
return s.toFixed(2) + ' ' + units[units.length - 1];
}
let fileData = [];
let parentDirData = null;
let sortColumn = null;
let sortDirection = 'asc';
function replaceTextInNode(node, searchText, replaceText) {
if (node.nodeType === Node.TEXT_NODE) {
if (node.textContent.includes(searchText)) {
node.textContent = node.textContent.replace(new RegExp(searchText, 'g'), replaceText);
}
} else {
for (let i = 0; i < node.childNodes.length; i++) {
replaceTextInNode(node.childNodes[i], searchText, replaceText);
}
}
}
const hiddenKeywords = ['@eaDir', '@info'];
function shouldHideEntry(name) {
if (!name) {
return false;
}
const lowerName = name.toLowerCase();
return hiddenKeywords.some((keyword) => lowerName.includes(keyword.toLowerCase()));
}
function updateHeadingBreadcrumb() {
const heading = document.querySelector('h1');
if (!heading) {
return;
}
const rawPath = window.location.pathname || '/';
const segments = rawPath
.split('/')
.filter((segment) => segment)
.map((segment) => decodeURIComponent(segment));
let accumulatedPath = '';
const parts = ['Index of'];
const appendParentLink = () => {
parts.push(' <a href="/">..</a>');
};
if (segments.length === 0) {
appendParentLink();
parts.push(' <span>/</span>');
heading.classList.add('yq-heading-breadcrumb');
heading.innerHTML = parts.join('');
return;
}
appendParentLink();
const encodedSegments = [];
segments.forEach((segment, index) => {
const encodedSegment = encodeURIComponent(segment);
encodedSegments.push(encodedSegment);
accumulatedPath = `/${encodedSegments.join('/')}`;
const isLast = index === segments.length - 1;
const href = accumulatedPath.endsWith('/') ? accumulatedPath : `${accumulatedPath}/`;
if (!isLast) {
parts.push(` / <a href="${href}">${segment}</a>`);
} else {
parts.push(` / <span>${segment}</span>`);
}
});
heading.classList.add('yq-heading-breadcrumb');
heading.innerHTML = parts.join('');
}
function parseFileSize(sizeText) {
if (!sizeText || sizeText === '-' || sizeText.trim() === '') {
return -1;
}
const cleanSize = sizeText.toString().replace(/,/g, '');
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let s = Number(cleanSize);
if (isNaN(s) || s < 0) {
return -1;
}
return s;
}
function parseDate(dateText) {
if (!dateText) return 0;
return new Date(dateText).getTime();
}
function sortFiles(column) {
if (sortColumn === column) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else {
sortColumn = column;
sortDirection = 'asc';
}
fileData.sort((a, b) => {
let comparison = 0;
if (column === 'name') {
comparison = a.name.localeCompare(b.name, 'zh-CN');
} else if (column === 'date') {
comparison = a.dateValue - b.dateValue;
} else if (column === 'size') {
comparison = a.sizeValue - b.sizeValue;
}
return sortDirection === 'asc' ? comparison : -comparison;
});
renderFiles();
updateSortIndicators();
}
function updateSortIndicators() {
document.querySelectorAll('.yq-header > div').forEach(header => {
header.classList.remove('sort-asc', 'sort-desc');
});
if (sortColumn === 'name') {
document.querySelector('.header-name').classList.add(sortDirection === 'asc' ? 'sort-asc' : 'sort-desc');
} else if (sortColumn === 'date') {
document.querySelector('.header-date').classList.add(sortDirection === 'asc' ? 'sort-asc' : 'sort-desc');
} else if (sortColumn === 'size') {
document.querySelector('.header-size').classList.add(sortDirection === 'asc' ? 'sort-asc' : 'sort-desc');
}
}
function renderFiles() {
const outerBox = document.querySelector('.yq-outer-box');
const headerRow = outerBox.querySelector('.yq-header');
outerBox.innerHTML = '';
outerBox.appendChild(headerRow);
// 先渲染 ../ 返回上级目录(如果存在)
if (parentDirData) {
const row = document.createElement('div');
row.classList.add('yq-row');
const name = document.createElement('a');
name.href = parentDirData.href;
name.innerText = parentDirData.name;
name.classList.add(parentDirData.className);
row.appendChild(name);
const lastModified = document.createElement('div');
lastModified.innerText = parentDirData.dateText || '';
lastModified.classList.add('last-modified');
row.appendChild(lastModified);
const size = document.createElement('div');
size.innerText = parentDirData.sizeText || '-';
size.classList.add('size');
row.appendChild(size);
outerBox.appendChild(row);
}
// 然后渲染其他文件
fileData.forEach(data => {
const row = document.createElement('div');
row.classList.add('yq-row');
const name = document.createElement('a');
name.href = data.href;
name.innerText = data.name;
name.classList.add(data.className);
row.appendChild(name);
const lastModified = document.createElement('div');
lastModified.innerText = data.dateText;
lastModified.classList.add('last-modified');
row.appendChild(lastModified);
const size = document.createElement('div');
size.innerText = data.sizeText;
size.classList.add('size');
row.appendChild(size);
outerBox.appendChild(row);
});
}
function change() {
const preElement = document.getElementsByTagName('pre')[0];
const preClassName = preElement.className;
if (preClassName) {
return;
}
const outerBox = document.createElement('div');
outerBox.classList.add('yq-outer-box');
// 添加标题行
const headerRow = document.createElement('div');
headerRow.classList.add('yq-header');
const headerName = document.createElement('div');
headerName.textContent = '\u6587\u4ef6\u540d';
headerName.classList.add('header-name');
headerName.onclick = () => sortFiles('name');
headerRow.appendChild(headerName);
const headerDate = document.createElement('div');
headerDate.textContent = '\u4fee\u6539\u65f6\u95f4';
headerDate.classList.add('header-date');
headerDate.onclick = () => sortFiles('date');
headerRow.appendChild(headerDate);
const headerSize = document.createElement('div');
headerSize.textContent = '\u5927\u5c0f';
headerSize.classList.add('header-size');
headerSize.onclick = () => sortFiles('size');
headerRow.appendChild(headerSize);
outerBox.appendChild(headerRow);
fileData = [];
parentDirData = null;
const currentPath = window.location.pathname || '/';
const isRootPath = !currentPath || currentPath === '/';
const isParentLink = (name, href) => {
const nameLower = (name || '').toLowerCase().trim();
const rawHref = href || '';
const decodedHref = decodeURIComponent(rawHref);
return (
nameLower === '../' ||
nameLower === '..' ||
nameLower === 'parent directory' ||
nameLower.includes('parent') ||
rawHref.includes('../') ||
rawHref.endsWith('..') ||
rawHref.includes('%2e%2e') ||
decodedHref.includes('../')
);
};
const createParentDataFallback = () => {
if (isRootPath) {
return null;
}
const trimmed = currentPath.endsWith('/') ? currentPath.slice(0, -1) : currentPath;
const lastSlash = trimmed.lastIndexOf('/');
const parentPath = lastSlash > 0 ? trimmed.slice(0, lastSlash + 1) : '/';
return {
href: parentPath,
name: '../',
className: 'folder',
isFolder: true,
dateText: '',
sizeText: '-',
dateValue: 0,
sizeValue: -1,
};
};
// 首先扫描所有 <a> 标签,尝试定位父目录链接
const allLinks = preElement.querySelectorAll('a');
allLinks.forEach((link) => {
const name = (link.text || link.innerText || link.textContent || '').trim();
if (shouldHideEntry(name)) {
return;
}
const rawHref = link.getAttribute('href') || '';
const resolvedHref = link.href || rawHref;
if (isParentLink(name, rawHref) || isParentLink(name, resolvedHref)) {
let nextNode = link.nextSibling;
while (nextNode && nextNode.nodeType !== Node.TEXT_NODE) {
nextNode = nextNode.nextSibling;
}
let dateText = '';
if (nextNode && nextNode.nodeType === Node.TEXT_NODE) {
const text = nextNode.nodeValue.trim();
if (text) {
const textArray = text.split(' ').filter((item) => item.trim());
dateText = formatDate(textArray[0], textArray[1]);
}
}
parentDirData = {
href: rawHref || resolvedHref || '../',
name: '../',
className: 'folder',
isFolder: true,
dateText,
sizeText: '-',
dateValue: parseDate(dateText),
sizeValue: -1,
};
}
});
let currentRow = null;
let isFolder = false;
preElement.childNodes.forEach((child) => {
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === 'A') {
const name = (child.text || child.innerText || child.textContent || '').trim();
if (shouldHideEntry(name)) {
currentRow = null;
isFolder = false;
return;
}
const rawHref = child.getAttribute('href') || '';
const resolvedHref = child.href || rawHref;
if (isParentLink(name, rawHref) || isParentLink(name, resolvedHref)) {
return;
}
currentRow = {
href: resolvedHref,
name: name,
className: getClassName(name),
isFolder: getClassName(name) === 'folder',
};
isFolder = currentRow.isFolder;
} else if (child.nodeType === Node.TEXT_NODE) {
if (!currentRow) {
return;
}
const text = child.nodeValue.trim();
if (!text) {
return;
}
const textArray = text.split(' ').filter((item) => item.trim());
const dateText = formatDate(textArray[0], textArray[1]);
const sizeText = isFolder ? '-' : formatSize(textArray[2] || textArray[textArray.length - 1] || '');
currentRow.dateText = dateText;
currentRow.sizeText = sizeText;
currentRow.dateValue = parseDate(dateText);
currentRow.sizeValue = isFolder ? -1 : parseFileSize(textArray[2] || textArray[textArray.length - 1] || '');
fileData.push(currentRow);
currentRow = null;
}
});
if (isRootPath) {
parentDirData = null;
} else if (!parentDirData) {
parentDirData = createParentDataFallback();
}
document.body.removeChild(preElement);
document.body.appendChild(outerBox);
renderFiles();
updateHeadingBreadcrumb();
}
// 页面加载完成后替换文本
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
change();
updateHeadingBreadcrumb();
});
} else {
change();
updateHeadingBreadcrumb();
}
const footer = document.createElement('div');
footer.className = 'yq-footer';
footer.innerText = '© myluzh';
document.body.appendChild(footer);
</script>
</body>
</html>