first commit
This commit is contained in:
commit
4df0d17183
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
.env
|
||||||
|
|
80
README.md
Normal file
80
README.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
Here is a `README.md` file for your project:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Raven Scott Blog Website
|
||||||
|
|
||||||
|
This repository contains the code for Raven Scott's blog website, where markdown files serve as blog posts, and visitors can browse through posts, learn more about Raven, and contact him directly.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
raven-scott-website
|
||||||
|
├── app.js # Main server-side logic using Express.js
|
||||||
|
├── markdown # Folder containing markdown files for blog posts
|
||||||
|
│ └── API Caching for Minecraft Servers.md
|
||||||
|
├── package-lock.json # Auto-generated package lock file
|
||||||
|
├── package.json # Dependencies and project metadata
|
||||||
|
├── public # Static files such as CSS, images
|
||||||
|
│ └── css
|
||||||
|
│ └── styles.css # Custom styles for the website
|
||||||
|
└── views # EJS template files for rendering HTML
|
||||||
|
├── about.ejs # 'About Me' page template
|
||||||
|
├── blog-post.ejs # Blog post page template
|
||||||
|
├── contact.ejs # Contact form page template
|
||||||
|
└── index.ejs # Homepage displaying recent blog posts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Markdown Blog Posts**: Blog posts are written in Markdown, located in the `markdown` folder. The content is rendered with [marked](https://www.npmjs.com/package/marked) and supports code syntax highlighting using [highlight.js](https://highlightjs.org/).
|
||||||
|
- **Dynamic Blog Post Display**: Each markdown file is dynamically converted into a blog post with a readable URL slug. The homepage shows the latest posts, and each post has its own dedicated page.
|
||||||
|
- **Contact Form**: Visitors can use the contact form to send inquiries directly. Messages are handled via `nodemailer`.
|
||||||
|
- **Responsive Design**: The website uses [Bootstrap](https://getbootstrap.com/) to ensure a clean and responsive layout across devices.
|
||||||
|
- **Professional Look and Feel**: The custom CSS in `styles.css` provides a dark theme with accent colors for a professional, modern look.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@git.ssh.surf:snxraven/ravenscott-blog.git
|
||||||
|
cd raven-scott-website
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create a `.env` file to set up environment variables (e.g., for the email service using `nodemailer`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
touch .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
PORT=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run the project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will run on [http://localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
|
## Project Details
|
||||||
|
|
||||||
|
- **Template Engine**: [EJS](https://ejs.co/) is used to render dynamic HTML views.
|
||||||
|
- **Markdown Parsing**: Blog posts are stored in the `markdown` folder, and the app converts them to HTML using `marked`.
|
||||||
|
- **Code Highlighting**: Any code snippets in the markdown files are automatically highlighted with `highlight.js`.
|
||||||
|
- **Routing**:
|
||||||
|
- `/`: Displays the homepage with the latest blog posts.
|
||||||
|
- `/about`: About Me page where Raven shares his passions and interests.
|
||||||
|
- `/contact`: Contact page where visitors can submit inquiries.
|
||||||
|
- `/blog/:slug`: Dynamically generated blog post pages based on the markdown file.
|
||||||
|
|
124
app.js
Normal file
124
app.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
require('dotenv').config(); // Load environment variables
|
||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { marked } = require('marked');
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
const hljs = require('highlight.js');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Set options for marked to use highlight.js for syntax highlighting
|
||||||
|
marked.setOptions({
|
||||||
|
highlight: function (code, language) {
|
||||||
|
// Check if the language is valid
|
||||||
|
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext';
|
||||||
|
return hljs.highlight(validLanguage, code).value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set EJS as templating engine
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
|
// Middleware to parse URL-encoded bodies (form submissions)
|
||||||
|
app.use(express.urlencoded({ extended: false }));
|
||||||
|
|
||||||
|
// Serve static files (CSS, Images)
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
|
// Function to load and parse markdown files and extract lead
|
||||||
|
function loadMarkdownWithLead(file) {
|
||||||
|
const markdownContent = fs.readFileSync(path.join(__dirname, 'markdown', file), 'utf-8');
|
||||||
|
|
||||||
|
let lead = '';
|
||||||
|
let contentMarkdown = markdownContent;
|
||||||
|
|
||||||
|
// Detect and extract the lead section
|
||||||
|
const leadKeyword = '<!-- lead -->';
|
||||||
|
if (contentMarkdown.includes(leadKeyword)) {
|
||||||
|
const [beforeLead, afterLead] = contentMarkdown.split(leadKeyword);
|
||||||
|
|
||||||
|
// Extract the first paragraph after the lead keyword
|
||||||
|
lead = afterLead.split('\n').find(line => line.trim() !== '').trim();
|
||||||
|
|
||||||
|
// Remove the lead from the main content
|
||||||
|
contentMarkdown = beforeLead + afterLead.replace(lead, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert markdown to HTML
|
||||||
|
const contentHtml = marked.parse(contentMarkdown);
|
||||||
|
|
||||||
|
return { contentHtml, lead };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to convert a title (with spaces) into a URL-friendly slug (with dashes)
|
||||||
|
function titleToSlug(title) {
|
||||||
|
return title.replace(/\s+/g, '-').toLowerCase(); // Always lowercase the slug
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to convert a slug (with dashes) back into a readable title (with spaces)
|
||||||
|
function slugToTitle(slug) {
|
||||||
|
return slug.replace(/-/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to load all blog posts
|
||||||
|
function getAllBlogPosts() {
|
||||||
|
const blogFiles = fs.readdirSync(path.join(__dirname, 'markdown')).filter(file => file.endsWith('.md'));
|
||||||
|
return blogFiles.map(file => {
|
||||||
|
const title = file.replace('.md', '').replace(/-/g, ' '); // Keep original casing for title
|
||||||
|
const slug = titleToSlug(title); // Convert title to slug (lowercase)
|
||||||
|
return {
|
||||||
|
title, // Original casing title
|
||||||
|
slug
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Home Route (Blog Home)
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
const blogPosts = getAllBlogPosts();
|
||||||
|
res.render('index', { title: 'Raven Scott Blog', blogPosts });
|
||||||
|
});
|
||||||
|
|
||||||
|
// About Route
|
||||||
|
app.get('/about', (req, res) => {
|
||||||
|
res.render('about', { title: 'About Raven Scott' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Display the Request a Quote form
|
||||||
|
app.get('/contact', (req, res) => {
|
||||||
|
res.render('contact', { title: 'Contact Raven Scott', msg: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Blog Post Route
|
||||||
|
app.get('/blog/:slug', (req, res) => {
|
||||||
|
const slug = req.params.slug;
|
||||||
|
const markdownFile = fs.readdirSync(path.join(__dirname, 'markdown'))
|
||||||
|
.find(file => titleToSlug(file.replace('.md', '')) === slug);
|
||||||
|
|
||||||
|
if (markdownFile) {
|
||||||
|
const originalTitle = markdownFile.replace('.md', ''); // Original title with casing
|
||||||
|
const blogPosts = getAllBlogPosts();
|
||||||
|
const { contentHtml, lead } = loadMarkdownWithLead(markdownFile);
|
||||||
|
|
||||||
|
res.render('blog-post', {
|
||||||
|
title: originalTitle, // Use the original title with casing
|
||||||
|
content: contentHtml,
|
||||||
|
lead: lead,
|
||||||
|
blogPosts
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(404).render('404', { title: 'Post not found' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request a Quote form remains unchanged
|
||||||
|
// ================================
|
||||||
|
// Server Listening
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server running on http://localhost:${PORT}`);
|
||||||
|
});
|
185
markdown/API Caching for Minecraft Servers.md
Normal file
185
markdown/API Caching for Minecraft Servers.md
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
<!-- lead -->
|
||||||
|
Finding an efficient way to cache data into memory.
|
||||||
|
|
||||||
|
# Building a Fast and Efficient Minecraft Server Cache Manager in Node.js
|
||||||
|
|
||||||
|
In this article, we will take an in-depth look at how to build a cache manager in Node.js to drastically improve the performance of your application, specifically for listing Minecraft servers. Without caching, the response time for listing all the servers was over 2 minutes due to multiple heavy computations and I/O operations. With the introduction of a caching mechanism, this time was reduced to an impressive 2 milliseconds. Let’s dive into the code and understand how this was achieved.
|
||||||
|
|
||||||
|
## Overview of the System
|
||||||
|
|
||||||
|
The purpose of this system is to manage a list of Minecraft servers efficiently. The core functionality includes fetching server information, such as the server's MOTD (Message of the Day), online status, game version, and operational details (e.g., ops, whitelist, banned players). The data is sourced from Docker containers, local file systems, and remote authentication services.
|
||||||
|
|
||||||
|
### Problem Statement
|
||||||
|
|
||||||
|
When you’re dealing with numerous Minecraft servers, querying real-time server data can become very expensive in terms of performance. Each query requires:
|
||||||
|
|
||||||
|
- Accessing container information via Docker.
|
||||||
|
- Checking server online status via network requests.
|
||||||
|
- Fetching MOTD and other server metadata.
|
||||||
|
- Interacting with the file system to fetch or generate tokens.
|
||||||
|
|
||||||
|
These operations, especially when scaled to multiple servers, can significantly slow down the response time. Without caching, the requests took over 2 minutes to process. This system mitigates those delays by introducing an efficient in-memory cache.
|
||||||
|
|
||||||
|
## The Caching Mechanism
|
||||||
|
|
||||||
|
The system employs a caching layer that stores various pieces of server information (MOTD, online status, etc.) in memory. This avoids repeated heavy I/O and network operations, thus drastically reducing the request time.
|
||||||
|
|
||||||
|
### Key Components of the Cache
|
||||||
|
|
||||||
|
1. **MOTD Cache:** Stores the Message of the Day for each server.
|
||||||
|
2. **Online Status Cache:** Stores whether a server is online or offline.
|
||||||
|
3. **Token Cache:** Stores authentication tokens for server owners.
|
||||||
|
4. **Container Info Cache:** Caches Docker container details for each server.
|
||||||
|
5. **Ops, Whitelist, Banned Players Caches:** Caches server-specific administrative data.
|
||||||
|
|
||||||
|
These caches are refreshed periodically and stored in an in-memory object, reducing the need for redundant calls to external systems.
|
||||||
|
|
||||||
|
## Code Walkthrough: The Cache Manager
|
||||||
|
|
||||||
|
### Directory and File Structure
|
||||||
|
|
||||||
|
- **Cache Directory:** The `cacheDir` (in this case, `/home/cache`) stores JSON files that provide metadata for each Minecraft server. Each file corresponds to a specific server and includes connection details and server-specific configuration data.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const cacheDir = "/home/cache";
|
||||||
|
const cache = {
|
||||||
|
motd: {},
|
||||||
|
online: {},
|
||||||
|
token: {},
|
||||||
|
containerInfo: {},
|
||||||
|
ops: {},
|
||||||
|
whitelist: {},
|
||||||
|
whitelistEnable: {},
|
||||||
|
banned: {}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The `cache` object holds the in-memory data for all servers, avoiding the need to repeatedly fetch this data from external sources.
|
||||||
|
|
||||||
|
### Server Listing Function
|
||||||
|
|
||||||
|
Here’s the code for listing all servers, including how caching is used to optimize performance:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
exports.list_all_servers = async function (req, res) {
|
||||||
|
let secret_key = req.params["secret_key"];
|
||||||
|
|
||||||
|
if (secret_key !== config.secret_key) {
|
||||||
|
return res.status(401).json({ success: false, error: "Unauthorized." });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const files = await fs.promises.readdir(cacheDir);
|
||||||
|
let jumpnode_servers = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(cacheDir, file);
|
||||||
|
const data = await jsonfile.readFile(filePath);
|
||||||
|
const serverName = path.basename(file, path.extname(file)).replace('.mc', '');
|
||||||
|
|
||||||
|
// Skip certain servers
|
||||||
|
if (serverName.includes(".link")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch data from cache or fallback to fetching from the source
|
||||||
|
const connectString = data.connect;
|
||||||
|
const motd = cache.motd[connectString]?.value ?? null;
|
||||||
|
const online = cache.online[connectString]?.value ?? false;
|
||||||
|
const token = cache.token[serverName]?.value ?? null;
|
||||||
|
const containerInfo = cache.containerInfo[serverName]?.value ?? null;
|
||||||
|
const ops = cache.ops[serverName]?.value ?? null;
|
||||||
|
const whitelist = cache.whitelist[serverName]?.value ?? null;
|
||||||
|
const whitelistEnable = cache.whitelistEnable[serverName]?.value ?? null;
|
||||||
|
const banned = cache.banned[serverName]?.value ?? null;
|
||||||
|
|
||||||
|
if (!containerInfo) {
|
||||||
|
continue; // Skip servers with no container info
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server information is stored in jumpnode_servers array
|
||||||
|
const serverInfo = {
|
||||||
|
serverName,
|
||||||
|
gameVersion: containerInfo.Config.Image.split(':').pop(),
|
||||||
|
connect: data.connect,
|
||||||
|
ownersToken: token,
|
||||||
|
ops,
|
||||||
|
whitelist,
|
||||||
|
whitelistEnable,
|
||||||
|
banlist: banned,
|
||||||
|
motd,
|
||||||
|
online
|
||||||
|
};
|
||||||
|
|
||||||
|
jumpnode_servers.push(serverInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({ success: true, servers: jumpnode_servers });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error reading cache directory:", err);
|
||||||
|
return res.status(500).json({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This function first checks the secret key for authentication. If the secret key is valid, it proceeds to list all servers. The server data is retrieved from the in-memory cache, avoiding the need to re-fetch the data from slow external sources like Docker, network connections, and the file system.
|
||||||
|
|
||||||
|
### Caching Core Functions
|
||||||
|
|
||||||
|
#### Fetching MOTD
|
||||||
|
|
||||||
|
The function `fetchMOTD` sends a custom status request packet to the server and waits for its response. The result is cached, so future requests can serve the MOTD immediately without querying the server again.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function fetchMOTD(connectString) {
|
||||||
|
const client = new net.Socket();
|
||||||
|
const serverPort = connectString.split(':')[1];
|
||||||
|
|
||||||
|
// Sends the packet to get the MOTD
|
||||||
|
client.connect(serverPort, 'my-mc.link', () => {
|
||||||
|
client.write(Buffer.from([0xFE, 0x01]));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process and cache the response
|
||||||
|
client.on('data', (data) => {
|
||||||
|
const response = data.toString('utf8').split('\x00\x00\x00');
|
||||||
|
if (response.length >= 6) {
|
||||||
|
const motd = response[3].replace(/\u0000/g, '');
|
||||||
|
cache.motd[connectString] = { value: motd, timestamp: Date.now() };
|
||||||
|
}
|
||||||
|
client.destroy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Fetching Container Info
|
||||||
|
|
||||||
|
The `fetchContainerInfo` function interacts with Docker to fetch container details for each Minecraft server. Dockerode is used to communicate with the Docker API. The result is cached in memory to avoid repeatedly querying Docker for container data.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function fetchContainerInfo(serverName) {
|
||||||
|
const container = docker.getContainer(serverName);
|
||||||
|
const containerInfo = await container.inspect();
|
||||||
|
cache.containerInfo[serverName] = { value: containerInfo, timestamp: Date.now() };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Other Cache Functions
|
||||||
|
|
||||||
|
Similar functions exist for fetching online status, tokens, ops, whitelist, and banned player lists, all of which follow the same caching pattern.
|
||||||
|
|
||||||
|
## Cache Update Strategy
|
||||||
|
|
||||||
|
The cache is updated by periodically calling the `updateCache` function, which refreshes the cache every 60 seconds:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
setInterval(updateCache, 60000);
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures that the cache is kept relatively up-to-date while still providing the performance benefits of avoiding repeated heavy I/O operations. The cache holds both the value and a timestamp, allowing future improvements like time-based cache invalidation if needed.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
By introducing an in-memory caching system, this application was able to reduce request times from over 2 minutes to just 2 milliseconds. This system efficiently caches key server data, eliminating the need to repeatedly query external services like Docker, network services, and file systems. This approach is especially useful in applications with high I/O overhead and network latency, allowing for faster and more responsive interactions.
|
||||||
|
|
||||||
|
This caching strategy could be adapted and extended further, for instance by adding cache expiration policies or integrating Redis for distributed caching in a multi-node architecture.
|
22
package.json
Normal file
22
package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "geeks",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "README.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.20.3",
|
||||||
|
"bootstrap": "^5.3.3",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"ejs": "^3.1.10",
|
||||||
|
"express": "^4.21.0",
|
||||||
|
"highlight.js": "^11.10.0",
|
||||||
|
"marked": "^14.1.2",
|
||||||
|
"nodemailer": "^6.9.15"
|
||||||
|
}
|
||||||
|
}
|
139
public/css/styles.css
Normal file
139
public/css/styles.css
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
body {
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary {
|
||||||
|
--bs-bg-opacity: 1;
|
||||||
|
background-color: rgb(0 0 0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background-color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav .nav-link {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background: #000000;
|
||||||
|
color: #fff;
|
||||||
|
padding: 2px 0;
|
||||||
|
text-align: center;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.lead {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #000000;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
background-color: #121212;
|
||||||
|
color: #fff;
|
||||||
|
padding: 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a {
|
||||||
|
color: #999;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Styles for Navbar and Dropdown */
|
||||||
|
.navbar {
|
||||||
|
background-color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav .nav-link {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
color: #ffffff;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav .nav-link:hover {
|
||||||
|
color: #2c5364;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Dropdown Styling */
|
||||||
|
.custom-dropdown {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-dropdown .dropdown-item {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-dropdown .dropdown-item:hover {
|
||||||
|
background-color: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-dropdown .dropdown-item:active {
|
||||||
|
background-color: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Toggler */
|
||||||
|
.navbar-toggler {
|
||||||
|
border-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler-icon {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
153
views/about.ejs
Normal file
153
views/about.ejs
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<!-- Meta and Title -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>About Me - Raven Scott</title>
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css">
|
||||||
|
<!-- Font Awesome CSS for Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<link rel="stylesheet" href="/css/styles.css">
|
||||||
|
<style>
|
||||||
|
/* Custom styles for a more professional look */
|
||||||
|
body {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background-color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-me h2,
|
||||||
|
.about-me h3 {
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-me p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #d1d1d1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-me {
|
||||||
|
padding: 50px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a {
|
||||||
|
color: #9a9a9a;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #007bff;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
border-color: #004085;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add padding for better text readability */
|
||||||
|
.about-me p {
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Separator style for sections */
|
||||||
|
.section-divider {
|
||||||
|
width: 80px;
|
||||||
|
height: 3px;
|
||||||
|
background-color: #007bff;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Navigation Bar -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">Raven Scott</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="/about">About Me</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/contact">Contact</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- About Me Section -->
|
||||||
|
<section class="about-me py-5">
|
||||||
|
<div class="container text-center">
|
||||||
|
<h2 class="text-white mb-4">About Me</h2>
|
||||||
|
<div class="section-divider"></div>
|
||||||
|
<p class="lead">Hi, I’m Raven Scott, a Linux enthusiast and problem solver with a deep passion for technology and creativity. I thrive in environments where I can learn, experiment, and turn ideas into reality. Whether it's building systems, coding, or tackling complex technical challenges, I find joy in using technology to make life easier and more efficient.</p>
|
||||||
|
|
||||||
|
<p>My passion for Linux and open-source technologies began early on, and since then, I’ve been on a continuous journey of growth and discovery. From troubleshooting networking issues to optimizing servers for performance, I love diving deep into the intricate details of how things work. The thrill of solving problems, especially when it comes to system security or performance optimization, is what fuels me every day.</p>
|
||||||
|
|
||||||
|
<h3 class="text-white mt-5">What Drives Me</h3>
|
||||||
|
<div class="section-divider"></div>
|
||||||
|
<p>I’m passionate about more than just the technical side. I believe in the power of technology to bring people together, and that’s why I’m dedicated to creating platforms and solutions that are accessible and impactful. Whether it's hosting services, developing peer-to-peer applications, or automating complex tasks, I’m always exploring new ways to push the boundaries of what's possible.</p>
|
||||||
|
|
||||||
|
<p>Outside of work, I love contributing to community projects and sharing my knowledge with others. Helping people grow their own skills is one of the most rewarding aspects of what I do. From mentoring to writing documentation, I’m constantly looking for ways to give back to the tech community.</p>
|
||||||
|
|
||||||
|
<h3 class="text-white mt-5">Creative Side</h3>
|
||||||
|
<div class="section-divider"></div>
|
||||||
|
<p>When I’m not deep in the technical world, I’m exploring my creative side through music. I run my own music label, where I produce and distribute AI-generated music across all platforms. Music and technology blend seamlessly for me, as both are outlets for innovation and expression.</p>
|
||||||
|
|
||||||
|
<p>In everything I do, from coding to creating music, my goal is to keep learning, growing, and sharing my passion with the world. If you ever want to connect, collaborate, or simply chat about tech or music, feel free to reach out!</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-dark text-white text-center py-4">
|
||||||
|
<div class="container">
|
||||||
|
<h4 class="footer-logo mb-3">Never Stop Learning</h4>
|
||||||
|
<p class="footer-links mb-3">
|
||||||
|
<a href="/" class="text-white text-decoration-none me-3">Home</a>
|
||||||
|
<a href="/about" class="text-white text-decoration-none me-3">About</a>
|
||||||
|
<a href="/contact" class="text-white text-decoration-none">Contact</a>
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">© 2024 Raven Scott. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS Bundle -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
77
views/blog-post.ejs
Normal file
77
views/blog-post.ejs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title %></title>
|
||||||
|
<!-- Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="/css/styles.css">
|
||||||
|
<!-- Highlight.js CSS for Syntax Highlighting -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/default.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">Raven Scott</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="/">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/about">About Me</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/contact">Contact</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="bg-primary text-white text-center py-5">
|
||||||
|
<h1><%= title %></h1>
|
||||||
|
<p class="lead"><%= lead %></p> <!-- Lead is dynamically set here -->
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container my-5">
|
||||||
|
<!-- Render Markdown content as HTML -->
|
||||||
|
<div class="markdown-content">
|
||||||
|
<%- content %>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-dark text-white text-center py-4">
|
||||||
|
<div class="container">
|
||||||
|
<h4 class="footer-logo mb-3">Never Stop Learning</h4>
|
||||||
|
<p class="footer-links mb-3">
|
||||||
|
<a href="/" class="text-white text-decoration-none me-3">Home</a>
|
||||||
|
<a href="/about" class="text-white text-decoration-none me-3">About</a>
|
||||||
|
<a href="/contact" class="text-white text-decoration-none">Contact</a>
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">© 2024 Raven Scott. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<!-- Highlight.js Script -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// Initialize Highlight.js
|
||||||
|
document.addEventListener('DOMContentLoaded', (event) => {
|
||||||
|
document.querySelectorAll('pre code').forEach((block) => {
|
||||||
|
hljs.highlightElement(block);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
96
views/contact.ejs
Normal file
96
views/contact.ejs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<!-- Meta and Title -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Contact Me - Raven Scott</title>
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css">
|
||||||
|
<!-- Font Awesome CSS for Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<link rel="stylesheet" href="/css/styles.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-dark text-white">
|
||||||
|
|
||||||
|
<!-- Navigation Bar -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">Raven Scott</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="/">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/about">About Me</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/contact">Contact</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Contact Me Section -->
|
||||||
|
<header class="d-flex align-items-center justify-content-center text-center py-5">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="mb-4 text-white">Contact Me</h2>
|
||||||
|
<p class="lead text-white">Have a question or need help with a project? Fill out the form below, and I'll be in touch!</p>
|
||||||
|
|
||||||
|
<!-- Display success or error message -->
|
||||||
|
<% if (typeof msg !== 'undefined') { %>
|
||||||
|
<div class="alert alert-info mt-3">
|
||||||
|
<%= msg %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<!-- Contact Form -->
|
||||||
|
<form action="/contact" method="POST" class="mt-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Your Name<span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control bg-dark text-white border-secondary" id="name" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">Email Address<span class="text-danger">*</span></label>
|
||||||
|
<input type="email" class="form-control bg-dark text-white border-secondary" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="subject" class="form-label">Subject<span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control bg-dark text-white border-secondary" id="subject" name="subject" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="message" class="form-label">Your Message<span class="text-danger">*</span></label>
|
||||||
|
<textarea class="form-control bg-dark text-white border-secondary" id="message" name="message" rows="6" required></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Send Message</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-dark text-white text-center py-4">
|
||||||
|
<div class="container">
|
||||||
|
<h4 class="footer-logo mb-3">Never Stop Learning</h4>
|
||||||
|
<p class="footer-links mb-3">
|
||||||
|
<a href="/" class="text-white text-decoration-none me-3">Home</a>
|
||||||
|
<a href="/about" class="text-white text-decoration-none me-3">About</a>
|
||||||
|
<a href="/contact" class="text-white text-decoration-none">Contact</a>
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">© 2024 Raven Scott. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS Bundle -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
74
views/index.ejs
Normal file
74
views/index.ejs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title %></title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="/css/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">raven-scott.fyi</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="/">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/about">About Me</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/contact">Contact</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
<header class="py-5">
|
||||||
|
<div class="container text-center">
|
||||||
|
<h1>Welcome to my long form post blog</h1>
|
||||||
|
<p class="lead">Latest articles and insights from Raven Scott</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="py-5">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Recent Posts</h2>
|
||||||
|
<div class="row">
|
||||||
|
<% blogPosts.forEach(post => { %>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title"><%= post.title %></h5>
|
||||||
|
<a href="/blog/<%= post.slug %>" class="btn btn-primary">Read More</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="bg-dark text-white text-center py-4">
|
||||||
|
<div class="container">
|
||||||
|
<h4 class="footer-logo mb-3">Never Stop Learning</h4>
|
||||||
|
<p class="footer-links mb-3">
|
||||||
|
<a href="/" class="text-white text-decoration-none me-3">Home</a>
|
||||||
|
<a href="/about" class="text-white text-decoration-none me-3">About</a>
|
||||||
|
<a href="/contact" class="text-white text-decoration-none">Contact</a>
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">© 2024 Raven Scott. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user