add dynamic seo settings

This commit is contained in:
2025-10-13 18:40:05 +02:00
parent 0d826e8fe2
commit 78af7b2906
4 changed files with 127 additions and 54 deletions

View File

@@ -11,6 +11,7 @@
"license": "MIT",
"dependencies": {
"clean-css": "^5.3.3",
"ejs": "^3.1.10",
"express": "^4.19.2",
"helmet": "^7.1.0",
"uglify-js": "^3.17.4",

View File

@@ -4,6 +4,7 @@ const path = require('path');
const express = require('express');
const helmet = require('helmet');
const winston = require('winston');
const ejs = require('ejs');
const DocumentHandler = require('./lib/document_handler.js');
const FileStorage = require('./lib/file_storage.js');
@@ -37,25 +38,6 @@ logger.info('Welcome to unknownBIN!');
// init file-storage
const fileStorage = new FileStorage({ path: config.dataPath, logger: logger });
// load static documents into file-storage
if (config.documents) {
for (const name in config.documents) {
const docPath = config.documents[name];
try {
const data = fs.readFileSync(docPath, 'utf8');
fileStorage.set(name, data, (success) => {
if (success) {
logger.verbose(`Loaded document: ${name} from ${docPath}`);
} else {
logger.warn(`Failed to store document: ${name}`);
}
});
} catch (err) {
logger.warn(`Unable to find or read document: ${name} at ${docPath}`);
}
}
}
// configure the document handler
const documentHandler = new DocumentHandler({
store: fileStorage,
@@ -68,9 +50,14 @@ const documentHandler = new DocumentHandler({
// setup routes and request-handling
const app = express();
// Configure EJS as the view engine
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// Use helmet for basic security headers
app.use(helmet());
// API routes
app.get('/raw/:id', (req, res) => {
return documentHandler.handleRawGet(req.params.id, res);
});
@@ -81,17 +68,47 @@ app.get('/documents/:id', (req, res) => {
return documentHandler.handleGet(req.params.id, res);
});
// Static files
app.use(express.static(path.join(__dirname, 'static')));
app.get('/:id', (req, res, next) => {
res.sendFile(path.join(__dirname, '/static/index.html'));
// Frontend routes (SSR)
app.get('/:id', (req, res) => {
const key = req.params.id;
// Don't treat static files as paste IDs
if (key.includes('.')) {
return res.status(404).send('Not Found');
}
fileStorage.get(key, (data) => {
if (data) {
// Paste found, render it with dynamic SEO tags
const title = `Paste ${key} - unknownBIN`;
const description = data.substring(0, 160).replace(/\n/g, ' ');
res.render('index', {
paste: {
key: key,
data: data,
title: title,
description: description
}
});
} else {
// Paste not found, render the homepage
res.render('index', {
paste: null
});
}
});
});
app.get('/', (req, res, next) => {
res.sendFile(path.join(__dirname, '/static/index.html'));
app.get('/', (req, res) => {
// Render the homepage
res.render('index', {
paste: null
});
});
app.listen(config.port, config.host, () => {
logger.info(`Listening on ${config.host}:${config.port}`);
});

View File

@@ -53,20 +53,40 @@ Haste.prototype.newDocument = function(hideHistory) {
// load an existing document
Haste.prototype.loadDocument = function(key) {
const _this = this;
_this.doc = new HasteDocument();
_this.doc.load(key, function(ret) {
if (ret) {
_this.$code.html(ret.value);
_this.setTitle(ret.key);
window.history.pushState(null, `${_this.appName}-${ret.key}`, `/${ret.key}`);
_this.fullKey();
_this.$textarea.val('').hide();
_this.$box.show();
} else {
_this.newDocument();
}
});
// Check if data is pre-rendered by the server
const $preloaded = $('#preloaded-data');
if ($preloaded.length > 0 && $preloaded.text().trim().length > 0) {
this.doc = new HasteDocument();
this.doc.locked = true;
this.doc.key = key;
this.doc.data = $preloaded.text();
const high = hljs.highlightAuto(this.doc.data).value;
const ret = {
value: high.replace(/.+/g, "<span class=\"line\">$&</span>").replace(/^\s*[\r\n]/gm, "<span class=\"line\"></span>\n"),
key: key,
};
this.$code.html(ret.value);
this.setTitle(ret.key);
this.fullKey();
this.$textarea.val('').hide();
this.$box.show();
} else {
// Data is not pre-rendered, fetch from API
const _this = this;
_this.doc = new HasteDocument();
_this.doc.load(key, function(ret) {
if (ret) {
_this.$code.html(ret.value);
_this.setTitle(ret.key);
window.history.pushState(null, `${_this.appName}-${ret.key}`, `/${ret.key}`);
_this.fullKey();
_this.$textarea.val('').hide();
_this.$box.show();
} else {
_this.newDocument();
}
});
}
};
// duplicate the current document
@@ -83,7 +103,10 @@ Haste.prototype.lockDocument = function() {
const _this = this;
this.doc.save(this.$textarea.val(), function(err, ret) {
if (!err && ret) {
_this.$code.html(ret.value.trim().replace(/.+/g, "<span class=\"line\">$&</span>").replace(/^\s*[\r\n]/gm, "<span class=\"line\"></span>\n"));
// Manually highlight and display after saving
const high = hljs.highlightAuto(ret.data).value;
const formatted = high.replace(/.+/g, "<span class=\"line\">$&</span>").replace(/^\s*[\r\n]/gm, "<span class=\"line\"></span>\n");
_this.$code.html(formatted);
_this.setTitle(ret.key);
window.history.pushState(null, `${_this.appName}-${ret.key}`, `/${ret.key}`);
_this.fullKey();
@@ -114,7 +137,7 @@ Haste.prototype.configureButtons = function() {
return evt.ctrlKey && evt.keyCode === 32;
},
action: function() {
_this.newDocument(!_this.doc.key);
window.location.href = '/';
}
},
{
@@ -172,15 +195,6 @@ const HasteDocument = function() {
this.locked = false;
};
// escape HTML-characters
HasteDocument.prototype.htmlEscape = function(s) {
return s
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.replace(/"/g, '&quot;');
};
// load a document from the server
HasteDocument.prototype.load = function(key, callback) {
const _this = this;
@@ -207,14 +221,18 @@ HasteDocument.prototype.load = function(key, callback) {
HasteDocument.prototype.save = function(data, callback) {
if (this.locked) return false;
this.data = data;
const _this = this;
_this.data = data;
$.ajax('/documents', {
type: 'post',
data: data,
dataType: 'json',
contentType: 'text/plain; charset=utf-8',
success: function(res) {
new Haste().loadDocument(res.key);
_this.locked = true;
_this.key = res.key;
// Pass the saved data back to the callback
callback(null, { key: res.key, data: _this.data });
},
error: function(res) {
try {
@@ -256,11 +274,11 @@ $(function() {
const app = new Haste();
const path = window.location.pathname;
if (path === '/') {
if (path === '/' || path === '') {
app.newDocument(true);
} else {
app.loadDocument(path.substring(1, path.length));
app.loadDocument(path.substring(1));
}
});
hljs.initHighlightingOnLoad();
// No need for hljs.initHighlightingOnLoad() as we do it manually

37
views/index.ejs Normal file
View File

@@ -0,0 +1,37 @@
// views/index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<% if (paste) { %>
<title><%= paste.title %></title>
<meta name="description" content="<%= paste.description %>">
<meta property="og:title" content="<%= paste.title %>">
<meta property="og:description" content="<%= paste.description %>">
<% } else { %>
<title>unknownBIN - A Secure Pastebin</title>
<meta name="description" content="unknownBIN is a secure and modern open-source Pastebin software written in Node.js.">
<meta property="og:title" content="unknownBIN - A Secure Pastebin">
<meta property="og:description" content="unknownBIN is a secure and modern open-source Pastebin software written in Node.js.">
<% } %>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="stylesheet" type="text/css" href="/application.min.css"/>
<link rel="stylesheet" type="text/css" href="/highlight.min.css"/>
<link rel="shortcut icon" href="/favicon.png">
<script src="/jquery.min.js"></script>
<script src="/highlight.min.js"></script>
<script src="/application.min.js"></script>
</head>
<body>
<div id="tools">
<div class="save function" title="Save [Ctrl + S]">Save</div>
<div class="new function" title="New [Ctrl + Space]">New</div>
<div class="duplicate function" title="Duplicate [Ctrl + D]">Duplicate</div>
<div class="raw function" title="Raw [Ctrl + Shift + R]">Raw</div>
</div>
<div id="content">
<pre id="code" style="display:none;" tabindex="0"><code <% if (paste) { %>id="preloaded-data"<% } %>><% if (paste) { %><%- paste.data %><% } %></code></pre>
<textarea spellcheck="false" style="display:none;"></textarea>
</div>
</body>
</html>