From aff1c20831bc70fbe37d821640629f5f1c6ee339 Mon Sep 17 00:00:00 2001
From: JMG Studios <214383483+JMGstudios@users.noreply.github.com>
Date: Fri, 24 Jul 2026 16:10:45 +0200
Subject: [PATCH 1/4] Updated developing guide to a fully working setup
---
.../hydrodactyl/local-development/lerd.mdx | 60 +++++++++++++++++--
1 file changed, 54 insertions(+), 6 deletions(-)
diff --git a/content/docs/hydrodactyl/local-development/lerd.mdx b/content/docs/hydrodactyl/local-development/lerd.mdx
index a4bc472..eaa3496 100644
--- a/content/docs/hydrodactyl/local-development/lerd.mdx
+++ b/content/docs/hydrodactyl/local-development/lerd.mdx
@@ -5,12 +5,8 @@ description: Setup a local development environment using Lerd
- The dev env currently is not 100% automated, some manual setup is required for the wings daemon
+ The dev env currently is not 100% automated, some manual setup is required for the wings daemon. See "installing wings"
-
- These docs are not finished yet, if you find something to fix or improve, make a pr!
-
-
## Prerequisites
@@ -19,7 +15,6 @@ description: Setup a local development environment using Lerd
- [pnpm](https://pnpm.io/)
## Initial setup
-
Note: Running a development environment on the same host as a production environment is not supported and will result in an error when connecting to port 80.
@@ -28,11 +23,64 @@ description: Setup a local development environment using Lerd
`git clone https://github.com/BlueprintFramework/hydrodactyl.git`
1. Cd into the Hydrodactyl directory
+
`cd hydrodactyl`
+
+
1. Run the dev env
`pnpm dev:setup`
1. Access Hydrodactyl at your new url
[`http://hydrodactyl.test`](http://hydrodactyl.test)
+
+ Your basic dev setup is now completed and you can login with **username** 'dev' and **password** 'dev'. Follow the guide below to setup wings and install a server.
+
+
+## Installing wings
+
+1. Follow along [`https://pterodactyl.io/wings/1.0/installing.html`](https://pterodactyl.io/wings/1.0/installing.html) to install wings on your local machine.
+
+1. As we are using https, we have to install local certificates:
+
+ ```bash
+ mkcert hydrodactyl.test
+ ```
+
+1. The certificates have been created in your home directory.
+ Create the certificate directory:
+
+ ```bash
+ mkdir -p /etc/pterodactyl/certs
+ ```
+
+1. Copy the certificates to the certs directory:
+
+ ```bash
+ cp ./hydrodactyl.test.pem \
+ /etc/pterodactyl/certs/fullchain.pem
+
+ cp ./hydrodactyl.test-key.pem \
+ /etc/pterodactyl/certs/privkey.pem
+ ```
+
+1. Run ```nano /etc/pterodactyl/config.yml```, search and enter the correct certificate paths:
+
+ ```yaml
+ ssl:
+ enabled: true
+ cert: /etc/pterodactyl/certs/fullchain.pem
+ key: /etc/pterodactyl/certs/privkey.pem
+ ```
+1. Start wings:
+
+ ```bash
+ wings --debug
+ ```
+
+1. Now you can add nodes and servers and start developing!
+
+
+ The error "Could not communicate with the machine running this server" means that Hydrodactyl has issues communicating with Wings. Check your Wings setup again!
+
\ No newline at end of file
From eb4739f3d8b5ad52a22940fe594b72549ac041c2 Mon Sep 17 00:00:00 2001
From: JMG Studios <214383483+JMGstudios@users.noreply.github.com>
Date: Wed, 29 Jul 2026 18:17:54 +0200
Subject: [PATCH 2/4] Added an automatically updating changelog
---
content/docs/hydrodactyl/changelog/index.mdx | 10 +
content/docs/hydrodactyl/changelog/meta.json | 4 +
.../{changelog.mdx => changelog/v5.mdx} | 4 +-
src/components/ui/Changelog.tsx | 478 ++++++++++++++++++
4 files changed, 494 insertions(+), 2 deletions(-)
create mode 100644 content/docs/hydrodactyl/changelog/index.mdx
create mode 100644 content/docs/hydrodactyl/changelog/meta.json
rename content/docs/hydrodactyl/{changelog.mdx => changelog/v5.mdx} (98%)
create mode 100644 src/components/ui/Changelog.tsx
diff --git a/content/docs/hydrodactyl/changelog/index.mdx b/content/docs/hydrodactyl/changelog/index.mdx
new file mode 100644
index 0000000..a4e6ac4
--- /dev/null
+++ b/content/docs/hydrodactyl/changelog/index.mdx
@@ -0,0 +1,10 @@
+---
+title: Changelog
+description: Hydrodactyl Changelog
+---
+
+import Changelog from '@/components/ui/Changelog';
+
+## Hydrodactyl Release Notes
+
+
diff --git a/content/docs/hydrodactyl/changelog/meta.json b/content/docs/hydrodactyl/changelog/meta.json
new file mode 100644
index 0000000..0a4eb57
--- /dev/null
+++ b/content/docs/hydrodactyl/changelog/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Changelog",
+ "pages": ["index"]
+}
diff --git a/content/docs/hydrodactyl/changelog.mdx b/content/docs/hydrodactyl/changelog/v5.mdx
similarity index 98%
rename from content/docs/hydrodactyl/changelog.mdx
rename to content/docs/hydrodactyl/changelog/v5.mdx
index 2ed30f1..9d32335 100644
--- a/content/docs/hydrodactyl/changelog.mdx
+++ b/content/docs/hydrodactyl/changelog/v5.mdx
@@ -1,6 +1,6 @@
---
-title: Changelog
-description: Hydrodactyl Changelog
+title: Changelog (v5 and earlier)
+description: Hydrodactyl Changelog for v5 and earlier releases
---
This file is a running track of new features and fixes to each version of the panel released starting with `v4.0.0`.
diff --git a/src/components/ui/Changelog.tsx b/src/components/ui/Changelog.tsx
new file mode 100644
index 0000000..813119c
--- /dev/null
+++ b/src/components/ui/Changelog.tsx
@@ -0,0 +1,478 @@
+'use client';
+
+import Link from 'next/link';
+import { useCallback, useEffect, useState, type ReactNode } from 'react';
+
+const PAGE_SIZE = 10; // this defines how many releases will be loaded at once
+const RELEASES_URL =
+ 'https://api.github.com/repos/blueprintframework/hydrodactyl/releases'; //should be clear what this is
+
+type ReleaseSummary = {
+ tagName: string;
+ publishedAt: string | null;
+ body: string;
+};
+
+type Block =
+ | { type: 'heading'; level: number; text: string }
+ | { type: 'list'; ordered: boolean; items: string[] }
+ | { type: 'paragraph'; text: string };
+
+function parseVersion(tagName: string) { //function to parse the version number
+ return tagName
+ .replace(/^v/i, '') //remove the 'v' in front
+ .split(/[.-]/) //split the version number (1.1.1 to 1 1 1)
+ .slice(0, 3) //only use the top three numbers ('1 1 1 beta' to '1 1 1')
+ .map((p) => parseInt(p, 10) || 0);
+}
+
+function isV6AndLater(tagName: string) { //sort out versions lower than 6.0.0 (if you go that far w/ pagination)
+ const [major = 0] = parseVersion(tagName);
+ return major >= 6;
+}
+
+function parseBlocks(body: string): Block[] {
+ const blocks: Block[] = [];
+ const lines = body.split(/\r?\n/); //split text on newline
+
+ let paragraphLines: string[] = [];
+ let listItems: string[] = [];
+ let listOrdered = false;
+
+ const flushParagraph = () => { //save the paragraph and flush buffer
+ if (paragraphLines.length) {
+ blocks.push({
+ type: 'paragraph',
+ text: paragraphLines.join(' '),
+ });
+ paragraphLines = [];
+ }
+ };
+
+ const flushList = () => { //save the list and flush buffer
+ if (listItems.length) {
+ blocks.push({
+ type: 'list',
+ ordered: listOrdered,
+ items: listItems,
+ });
+
+ listItems = [];
+ listOrdered = false;
+ }
+ };
+
+ for (const rawLine of lines) {
+ const line = rawLine.trim(); //remove empty spaces or wtv these are called ' '
+
+ /*
+ *An empty line marks the end of a paragraph/list!
+ */
+ if (!line) { //check if the line is empty
+ flushParagraph();
+ flushList();
+ continue;
+ } //save the paragraph/list, done.
+
+ const headingMatch = line.match(/^(#{1,6})\s+(.*)$/); //check if the line is a heading (starts with #)
+
+ if (headingMatch) {
+ flushParagraph();
+ flushList(); //save preceded paragraphs/lists
+
+ blocks.push({ //save the heading
+ type: 'heading',
+ level: headingMatch[1].length, //aka 1 - 6 #
+ text: headingMatch[2].trim(), //aka the content of the heading
+ });
+
+ continue;
+ }
+
+ const bulletMatch = line.match(/^[-*+]\s+(.*)$/); //detect markdown lists with -, * and + (unordered)
+ const orderedMatch = line.match(/^\d+\.\s+(.*)$/); //detect numeric lists (ordered)
+
+ if (bulletMatch || orderedMatch) {
+ flushParagraph(); //save preceded paragraphs
+
+ if (!listItems.length) {
+ listOrdered = Boolean(orderedMatch); //remember if the list is ordered or unordered (via the first item)
+ }
+
+ listItems.push((bulletMatch ?? orderedMatch)![1].trim()); /*
+ *1. Take bulletMatch if existing, else take orderedMatch
+ *2. Take the content of the list item
+ *3. Remove empty spaces at the start and end
+ */
+
+ continue;
+ }
+
+ flushList(); /*if the line is none of the previous cases:
+ *- Save any potential lists and*/
+ paragraphLines.push(line); //- Add the line to the current paragraph buffer
+ }
+
+ flushParagraph(); //obviously still have to save the paragraph/list if nothing special happened and they are not ended through an empty line
+ flushList();
+
+ return blocks;
+}
+
+function linkProps(href: string) { //helper to display and open links properly
+ return {
+ href,
+ target: '_blank' as const,
+ rel: 'noreferrer noopener',
+ className: 'text-fd-accent hover:underline break-all',
+ };
+}
+
+function shortenRepoReference(url: string): string | null { //helper to display long links as simple issue or PR hashtags, as you're used to from Github
+ const match = url.match(
+ /^https?:\/\/github\.com\/BlueprintFramework\/hydrodactyl\/(?:pull|issues)\/(\d+)(?:\/.*)?(?:[?#].*)?$/i
+ );
+
+ return match ? `#${match[1]}` : null;
+}
+
+function linkLabel(url: string, explicitLabel?: string) { //helper to display links
+ if (explicitLabel && explicitLabel !== url) {
+ return explicitLabel;
+ }
+
+ return shortenRepoReference(url) ?? explicitLabel ?? url;
+}
+
+function splitTrailingPunctuation(url: string) { //helper to decide between the actual link and trailing punctuation (a '.' after the link)
+ const match = url.match(/^(.*?)([.,;:!?)\]}>]+)$/);
+
+ if (!match) {
+ return { url, trailing: '' }; //if no trailing was found, return the url back and obv an empty trailing
+ }
+
+ let cleaned = match[1];
+ let trailing = match[2];
+
+ while (trailing.startsWith(')') && cleaned.includes('(')) { //keep balanced trailing ) if an opening ( exists in the URL
+ const open = (cleaned.match(/\(/g) ?? []).length;
+ const close = (cleaned.match(/\)/g) ?? []).length;
+
+ if (open > close) {
+ cleaned += ')';
+ trailing = trailing.slice(1);
+ } else {
+ break;
+ }
+ }
+
+ return { url: cleaned, trailing };
+}
+
+
+//==========================================MARKDOWN RENDERER=====================================================================
+function renderInline(
+ text: string,
+ keyPrefix: string
+): ReactNode[] {
+ const nodes: ReactNode[] = [];
+
+ const regex =
+ //searching for a bunch of stuff that matters for inline rendering
+ /\[([^\]]+)\]\(([^)]+)\)|\*\*([^*]+)\*\*|`([^`]+)`|\*([^*]+)\*|@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\[[^\]]+\])?)|(https?:\/\/[^\s<]+)/g;
+
+ let lastIndex = 0;
+ let match: RegExpExecArray | null;
+ let i = 0;
+
+ while ((match = regex.exec(text)) !== null) {
+ if (match.index > lastIndex) {
+ nodes.push(text.slice(lastIndex, match.index));
+ }
+
+ if (match[1]) {
+ nodes.push(
+
+ {linkLabel(match[2], match[1])}
+
+ );
+ } else if (match[3]) {
+ nodes.push(
+
+ {match[3]}
+
+ );
+ } else if (match[4]) {
+ nodes.push(
+
+ {match[4]}
+
+ );
+ } else if (match[5]) {
+ nodes.push(
+
+ {match[5]}
+
+ );
+ } else if (match[0].startsWith('@')) {
+ const username = match[0].slice(1).replace(/\[bot\]$/i, '');
+
+ nodes.push(
+
+ {match[0]}
+
+ );
+ } else if (match[6]) {
+ const { url, trailing } = splitTrailingPunctuation(match[6]);
+
+ nodes.push(
+
+ {linkLabel(url)}
+
+ );
+
+ if (trailing) {
+ nodes.push(trailing);
+ }
+ }
+
+ lastIndex = regex.lastIndex;
+ }
+
+ if (lastIndex < text.length) {
+ nodes.push(text.slice(lastIndex));
+ }
+
+ return nodes;
+}
+
+function renderBlocks(blocks: Block[]): ReactNode {
+ return blocks.map((block, index) => {
+ const key = `block-${index}`;
+
+ if (block.type === 'heading') {
+ return (
+