Merge pull request 'Window_PM2' (#3) from Window_PM2 into main
Some checks failed
Deploy / build-and-deploy (push) Failing after 0s
Some checks failed
Deploy / build-and-deploy (push) Failing after 0s
Reviewed-on: http://10.0.0.13:3002/rattatwinko/markdownblog/pulls/3 Ohhh lawd he comin. If nothing works then blame the compiler.
This commit is contained in:
@@ -12,16 +12,13 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Install Docker
|
||||||
uses: actions/setup-node@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Build Docker image
|
||||||
run: npm install
|
run: docker build -t markdownblog .
|
||||||
|
|
||||||
- name: Build project
|
- name: Push Docker image
|
||||||
run: npm run build
|
run: docker push registry.gitea.com/user/markdownblog:latest
|
||||||
|
|
||||||
- name: Start application
|
- name: Deploy to Gitea
|
||||||
run: npm start -- --port 1337
|
|
||||||
@@ -14,6 +14,10 @@ VOLUME ["/app/docker"]
|
|||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "start"]
|
||||||
|
|
||||||
# Building Instructions
|
# Building Instructions
|
||||||
|
|||||||
36
docker.sh
Executable file
36
docker.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
IMAGE_NAME="markdownblog"
|
||||||
|
CONTAINER_NAME="markdownblog"
|
||||||
|
VOLUME_NAME="markdownblog-posts"
|
||||||
|
PORT="8080"
|
||||||
|
|
||||||
|
# Stop and remove any containers using the volume
|
||||||
|
VOLUME_CONTAINERS=$(docker ps -a --filter volume=$VOLUME_NAME -q)
|
||||||
|
if [ -n "$VOLUME_CONTAINERS" ]; then
|
||||||
|
echo "Stopping and removing containers using the volume..."
|
||||||
|
docker rm -f $VOLUME_CONTAINERS
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove the volume for a clean start (optional, comment out if you want to keep posts)
|
||||||
|
echo "Removing Docker volume (for a clean start)..."
|
||||||
|
docker volume rm $VOLUME_NAME 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "Building Docker image..."
|
||||||
|
docker build -t $IMAGE_NAME .
|
||||||
|
|
||||||
|
echo "Running new container with persistent volume..."
|
||||||
|
docker run -d \
|
||||||
|
--name $CONTAINER_NAME \
|
||||||
|
-p $PORT:3000 \
|
||||||
|
-v $VOLUME_NAME:/app/docker \
|
||||||
|
$IMAGE_NAME
|
||||||
|
|
||||||
|
# Copy built-in posts to the volume if it's empty (one-time init)
|
||||||
|
echo "Copying built-in posts to Docker volume if empty..."
|
||||||
|
docker exec $CONTAINER_NAME sh -c 'if [ -d /app/posts ] && [ -d /app/docker ] && [ "$(ls -A /app/docker)" = "" ]; then cp -r /app/posts/* /app/docker/; fi'
|
||||||
|
|
||||||
|
echo "Deployment complete!"
|
||||||
|
echo "App should be available at http://localhost:$PORT"
|
||||||
6
entrypoint.sh
Normal file
6
entrypoint.sh
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Copy built-in posts to docker volume if the volume is empty
|
||||||
|
if [ -d /app/posts ] && [ -d /app/docker ] && [ "$(ls -A /app/docker)" = "" ]; then
|
||||||
|
cp -r /app/posts/* /app/docker/
|
||||||
|
fi
|
||||||
|
exec "$@"
|
||||||
1193
package-lock.json
generated
1193
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@
|
|||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"marked": "^12.0.0",
|
"marked": "^12.0.0",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
|
"pm2": "^6.0.8",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@types/jsdom": "^21.1.6",
|
"@types/jsdom": "^21.1.6",
|
||||||
"@types/marked": "^5.0.2",
|
"@types/marked": "^5.0.2",
|
||||||
|
"@types/tar": "^6.1.13",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-next": "14.1.0"
|
"eslint-config-next": "14.1.0"
|
||||||
|
|||||||
425
posts/mdtest.md
425
posts/mdtest.md
@@ -1,156 +1,319 @@
|
|||||||
---
|
---
|
||||||
title: "Markdown Testing"
|
title: Markdown Demo!
|
||||||
date: "2025-06-17"
|
date: '2025-06-19'
|
||||||
tags: ["testing"]
|
tags:
|
||||||
summary: "Testing Markdown Features"
|
- demo
|
||||||
---
|
summary: Demo of Markdown Parsing
|
||||||
# Markdown Feature Test
|
author: Rattatwinko's
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
- [Headings](#headings)
|
|
||||||
- [Text Formatting](#text-formatting)
|
|
||||||
- [Lists](#lists)
|
|
||||||
- [Links](#links)
|
|
||||||
- [Images](#images)
|
|
||||||
- [Code](#code)
|
|
||||||
- [Blockquotes](#blockquotes)
|
|
||||||
- [Horizontal Rule](#horizontal-rule)
|
|
||||||
- [Tables](#tables)
|
|
||||||
- [Task Lists](#task-lists)
|
|
||||||
- [HTML in Markdown](#html-in-markdown)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Headings
|
# Markdown: Syntax
|
||||||
|
|
||||||
# H1
|
* [Overview](#overview)
|
||||||
## H2
|
* [Philosophy](#philosophy)
|
||||||
### H3
|
* [Inline HTML](#html)
|
||||||
#### H4
|
* [Automatic Escaping for Special Characters](#autoescape)
|
||||||
##### H5
|
* [Block Elements](#block)
|
||||||
###### H6
|
* [Paragraphs and Line Breaks](#p)
|
||||||
|
* [Headers](#header)
|
||||||
---
|
* [Blockquotes](#blockquote)
|
||||||
|
* [Lists](#list)
|
||||||
## Text Formatting
|
* [Code Blocks](#precode)
|
||||||
|
* [Horizontal Rules](#hr)
|
||||||
- **Bold**
|
* [Span Elements](#span)
|
||||||
- *Italic*
|
* [Links](#link)
|
||||||
- ***Bold and Italic***
|
* [Emphasis](#em)
|
||||||
- ~~Strikethrough~~
|
* [Code](#code)
|
||||||
- <u>Underline (HTML)</u>
|
* [Images](#img)
|
||||||
- ==Highlight (non-standard)==
|
* [Miscellaneous](#misc)
|
||||||
|
* [Backslash Escapes](#backslash)
|
||||||
---
|
* [Automatic Links](#autolink)
|
||||||
|
|
||||||
## Lists
|
|
||||||
|
|
||||||
### Unordered
|
|
||||||
- Item 1
|
|
||||||
- Subitem 1.1
|
|
||||||
- Subitem 1.1.1
|
|
||||||
- Item 2
|
|
||||||
|
|
||||||
### Ordered
|
|
||||||
1. First item
|
|
||||||
2. Second item
|
|
||||||
1. Subitem
|
|
||||||
2. Subitem
|
|
||||||
3. Third item
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- [Inline Link](https://www.example.com)
|
|
||||||
- [Reference Link][example]
|
|
||||||
- <https://www.example.com>
|
|
||||||
|
|
||||||
[example]: https://www.example.com
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Images
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
GIF (Image works too):
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code
|
|
||||||
|
|
||||||
### Inline Code
|
|
||||||
|
|
||||||
Here is `inline code`.
|
|
||||||
|
|
||||||
### Code Block (fenced)
|
|
||||||
|
|
||||||
```Python
|
|
||||||
|
|
||||||
def hello\_world():
|
|
||||||
print("Hello, world!")
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Blog (test)
|
|
||||||
|
|
||||||
```Python
|
|
||||||
def hello():
|
|
||||||
return true;
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Code Block (indented)
|
**Note:** This document is itself written using Markdown; you
|
||||||
```Python
|
can [see the source for it by adding '.text' to the URL](/projects/markdown/syntax.text).
|
||||||
def indented_example():
|
|
||||||
return True
|
|
||||||
```
|
|
||||||
---
|
|
||||||
|
|
||||||
## Blockquotes
|
----
|
||||||
|
|
||||||
> This is a blockquote.
|
## Overview
|
||||||
|
|
||||||
|
### Philosophy
|
||||||
|
|
||||||
|
Markdown is intended to be as easy-to-read and easy-to-write as is feasible.
|
||||||
|
|
||||||
|
Readability, however, is emphasized above all else. A Markdown-formatted
|
||||||
|
document should be publishable as-is, as plain text, without looking
|
||||||
|
like it's been marked up with tags or formatting instructions. While
|
||||||
|
Markdown's syntax has been influenced by several existing text-to-HTML
|
||||||
|
filters -- including [Setext](http://docutils.sourceforge.net/mirror/setext.html), [atx](http://www.aaronsw.com/2002/atx/), [Textile](http://textism.com/tools/textile/), [reStructuredText](http://docutils.sourceforge.net/rst.html),
|
||||||
|
[Grutatext](http://www.triptico.com/software/grutatxt.html), and [EtText](http://ettext.taint.org/doc/) -- the single biggest source of
|
||||||
|
inspiration for Markdown's syntax is the format of plain text email.
|
||||||
|
|
||||||
|
## Block Elements
|
||||||
|
|
||||||
|
### Paragraphs and Line Breaks
|
||||||
|
|
||||||
|
A paragraph is simply one or more consecutive lines of text, separated
|
||||||
|
by one or more blank lines. (A blank line is any line that looks like a
|
||||||
|
blank line -- a line containing nothing but spaces or tabs is considered
|
||||||
|
blank.) Normal paragraphs should not be indented with spaces or tabs.
|
||||||
|
|
||||||
|
The implication of the "one or more consecutive lines of text" rule is
|
||||||
|
that Markdown supports "hard-wrapped" text paragraphs. This differs
|
||||||
|
significantly from most other text-to-HTML formatters (including Movable
|
||||||
|
Type's "Convert Line Breaks" option) which translate every line break
|
||||||
|
character in a paragraph into a `<br />` tag.
|
||||||
|
|
||||||
|
When you *do* want to insert a `<br />` break tag using Markdown, you
|
||||||
|
end a line with two or more spaces, then type return.
|
||||||
|
|
||||||
|
### Headers
|
||||||
|
|
||||||
|
Markdown supports two styles of headers, [Setext] [1] and [atx] [2].
|
||||||
|
|
||||||
|
Optionally, you may "close" atx-style headers. This is purely
|
||||||
|
cosmetic -- you can use this if you think it looks better. The
|
||||||
|
closing hashes don't even need to match the number of hashes
|
||||||
|
used to open the header. (The number of opening hashes
|
||||||
|
determines the header level.)
|
||||||
|
|
||||||
|
|
||||||
|
### Blockquotes
|
||||||
|
|
||||||
|
Markdown uses email-style `>` characters for blockquoting. If you're
|
||||||
|
familiar with quoting passages of text in an email message, then you
|
||||||
|
know how to create a blockquote in Markdown. It looks best if you hard
|
||||||
|
wrap the text and put a `>` before every line:
|
||||||
|
|
||||||
|
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
||||||
|
> consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
|
||||||
|
> Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
|
||||||
|
>
|
||||||
|
> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
|
||||||
|
> id sem consectetuer libero luctus adipiscing.
|
||||||
|
|
||||||
|
Markdown allows you to be lazy and only put the `>` before the first
|
||||||
|
line of a hard-wrapped paragraph:
|
||||||
|
|
||||||
|
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
|
||||||
|
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
|
||||||
|
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
|
||||||
|
|
||||||
|
> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
|
||||||
|
id sem consectetuer libero luctus adipiscing.
|
||||||
|
|
||||||
|
Blockquotes can be nested (i.e. a blockquote-in-a-blockquote) by
|
||||||
|
adding additional levels of `>`:
|
||||||
|
|
||||||
|
> This is the first level of quoting.
|
||||||
>
|
>
|
||||||
> > Nested blockquote.
|
> > This is nested blockquote.
|
||||||
|
>
|
||||||
|
> Back to the first level.
|
||||||
|
|
||||||
---
|
Blockquotes can contain other Markdown elements, including headers, lists,
|
||||||
|
and code blocks:
|
||||||
|
|
||||||
## Horizontal Rule
|
> ## This is a header.
|
||||||
|
>
|
||||||
|
> 1. This is the first list item.
|
||||||
|
> 2. This is the second list item.
|
||||||
|
>
|
||||||
|
> Here's some example code:
|
||||||
|
>
|
||||||
|
> return shell_exec("echo $input | $markdown_script");
|
||||||
|
|
||||||
---
|
Any decent text editor should make email-style quoting easy. For
|
||||||
|
example, with BBEdit, you can make a selection and choose Increase
|
||||||
|
Quote Level from the Text menu.
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
***
|
### Lists
|
||||||
|
|
||||||
---
|
Markdown supports ordered (numbered) and unordered (bulleted) lists.
|
||||||
|
|
||||||
## Tables
|
Unordered lists use asterisks, pluses, and hyphens -- interchangably
|
||||||
|
-- as list markers:
|
||||||
|
|
||||||
| Syntax | Description |
|
* Red
|
||||||
|--------|-------------|
|
* Green
|
||||||
| Header | Title |
|
* Blue
|
||||||
| Cell | Content |
|
|
||||||
|
|
||||||
---
|
is equivalent to:
|
||||||
|
|
||||||
## Task Lists
|
+ Red
|
||||||
|
+ Green
|
||||||
|
+ Blue
|
||||||
|
|
||||||
- [x] Task 1
|
and:
|
||||||
- [ ] Task 2
|
|
||||||
- [x] Subtask
|
|
||||||
- [ ] Subtask
|
|
||||||
|
|
||||||
---
|
- Red
|
||||||
|
- Green
|
||||||
|
- Blue
|
||||||
|
|
||||||
## HTML in Markdown
|
Ordered lists use numbers followed by periods:
|
||||||
|
|
||||||
<div style="color: red; font-weight: bold;">
|
1. Bird
|
||||||
This is raw HTML in Markdown.
|
2. McHale
|
||||||
</div>
|
3. Parish
|
||||||
|
|
||||||
---
|
It's important to note that the actual numbers you use to mark the
|
||||||
|
list have no effect on the HTML output Markdown produces. The HTML
|
||||||
|
Markdown produces from the above list is:
|
||||||
|
|
||||||
_End of Markdown Feature Test_
|
If you instead wrote the list in Markdown like this:
|
||||||
|
|
||||||
|
1. Bird
|
||||||
|
1. McHale
|
||||||
|
1. Parish
|
||||||
|
|
||||||
|
or even:
|
||||||
|
|
||||||
|
3. Bird
|
||||||
|
1. McHale
|
||||||
|
8. Parish
|
||||||
|
|
||||||
|
you'd get the exact same HTML output. The point is, if you want to,
|
||||||
|
you can use ordinal numbers in your ordered Markdown lists, so that
|
||||||
|
the numbers in your source match the numbers in your published HTML.
|
||||||
|
But if you want to be lazy, you don't have to.
|
||||||
|
|
||||||
|
To make lists look nice, you can wrap items with hanging indents:
|
||||||
|
|
||||||
|
* Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
|
||||||
|
Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,
|
||||||
|
viverra nec, fringilla in, laoreet vitae, risus.
|
||||||
|
* Donec sit amet nisl. Aliquam semper ipsum sit amet velit.
|
||||||
|
Suspendisse id sem consectetuer libero luctus adipiscing.
|
||||||
|
|
||||||
|
But if you want to be lazy, you don't have to:
|
||||||
|
|
||||||
|
* Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
|
||||||
|
Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,
|
||||||
|
viverra nec, fringilla in, laoreet vitae, risus.
|
||||||
|
* Donec sit amet nisl. Aliquam semper ipsum sit amet velit.
|
||||||
|
Suspendisse id sem consectetuer libero luctus adipiscing.
|
||||||
|
|
||||||
|
List items may consist of multiple paragraphs. Each subsequent
|
||||||
|
paragraph in a list item must be indented by either 4 spaces
|
||||||
|
or one tab:
|
||||||
|
|
||||||
|
1. This is a list item with two paragraphs. Lorem ipsum dolor
|
||||||
|
sit amet, consectetuer adipiscing elit. Aliquam hendrerit
|
||||||
|
mi posuere lectus.
|
||||||
|
|
||||||
|
Vestibulum enim wisi, viverra nec, fringilla in, laoreet
|
||||||
|
vitae, risus. Donec sit amet nisl. Aliquam semper ipsum
|
||||||
|
sit amet velit.
|
||||||
|
|
||||||
|
2. Suspendisse id sem consectetuer libero luctus adipiscing.
|
||||||
|
|
||||||
|
It looks nice if you indent every line of the subsequent
|
||||||
|
paragraphs, but here again, Markdown will allow you to be
|
||||||
|
lazy:
|
||||||
|
|
||||||
|
* This is a list item with two paragraphs.
|
||||||
|
|
||||||
|
This is the second paragraph in the list item. You're
|
||||||
|
only required to indent the first line. Lorem ipsum dolor
|
||||||
|
sit amet, consectetuer adipiscing elit.
|
||||||
|
|
||||||
|
* Another item in the same list.
|
||||||
|
|
||||||
|
To put a blockquote within a list item, the blockquote's `>`
|
||||||
|
delimiters need to be indented:
|
||||||
|
|
||||||
|
* A list item with a blockquote:
|
||||||
|
|
||||||
|
> This is a blockquote
|
||||||
|
> inside a list item.
|
||||||
|
|
||||||
|
To put a code block within a list item, the code block needs
|
||||||
|
to be indented *twice* -- 8 spaces or two tabs:
|
||||||
|
|
||||||
|
* A list item with a code block:
|
||||||
|
|
||||||
|
<code goes here>
|
||||||
|
|
||||||
|
### Code Blocks
|
||||||
|
|
||||||
|
Pre-formatted code blocks are used for writing about programming or
|
||||||
|
markup source code. Rather than forming normal paragraphs, the lines
|
||||||
|
of a code block are interpreted literally. Markdown wraps a code block
|
||||||
|
in both `<pre>` and `<code>` tags.
|
||||||
|
|
||||||
|
To produce a code block in Markdown, simply indent every line of the
|
||||||
|
block by at least 4 spaces or 1 tab.
|
||||||
|
|
||||||
|
This is a normal paragraph:
|
||||||
|
|
||||||
|
This is a code block.
|
||||||
|
|
||||||
|
Here is an example of AppleScript:
|
||||||
|
|
||||||
|
tell application "Foo"
|
||||||
|
beep
|
||||||
|
end tell
|
||||||
|
|
||||||
|
A code block continues until it reaches a line that is not indented
|
||||||
|
(or the end of the article).
|
||||||
|
|
||||||
|
Within a code block, ampersands (`&`) and angle brackets (`<` and `>`)
|
||||||
|
are automatically converted into HTML entities. This makes it very
|
||||||
|
easy to include example HTML source code using Markdown -- just paste
|
||||||
|
it and indent it, and Markdown will handle the hassle of encoding the
|
||||||
|
ampersands and angle brackets. For example, this:
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
© 2004 Foo Corporation
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Regular Markdown syntax is not processed within code blocks. E.g.,
|
||||||
|
asterisks are just literal asterisks within a code block. This means
|
||||||
|
it's also easy to use Markdown to write about Markdown's own syntax.
|
||||||
|
|
||||||
|
```
|
||||||
|
tell application "Foo"
|
||||||
|
beep
|
||||||
|
end tell
|
||||||
|
```
|
||||||
|
|
||||||
|
## Span Elements
|
||||||
|
|
||||||
|
### Links
|
||||||
|
|
||||||
|
Markdown supports two style of links: *inline* and *reference*.
|
||||||
|
|
||||||
|
In both styles, the link text is delimited by [square brackets].
|
||||||
|
|
||||||
|
To create an inline link, use a set of regular parentheses immediately
|
||||||
|
after the link text's closing square bracket. Inside the parentheses,
|
||||||
|
put the URL where you want the link to point, along with an *optional*
|
||||||
|
title for the link, surrounded in quotes. For example:
|
||||||
|
|
||||||
|
This is [an example](http://example.com/) inline link.
|
||||||
|
|
||||||
|
[This link](http://example.net/) has no title attribute.
|
||||||
|
|
||||||
|
### Emphasis
|
||||||
|
|
||||||
|
Markdown treats asterisks (`*`) and underscores (`_`) as indicators of
|
||||||
|
emphasis. Text wrapped with one `*` or `_` will be wrapped with an
|
||||||
|
HTML `<em>` tag; double `*`'s or `_`'s will be wrapped with an HTML
|
||||||
|
`<strong>` tag. E.g., this input:
|
||||||
|
|
||||||
|
*single asterisks*
|
||||||
|
|
||||||
|
_single underscores_
|
||||||
|
|
||||||
|
**double asterisks**
|
||||||
|
|
||||||
|
__double underscores__
|
||||||
|
|
||||||
|
### Code
|
||||||
|
|
||||||
|
To indicate a span of code, wrap it with backtick quotes (`` ` ``).
|
||||||
|
Unlike a pre-formatted code block, a code span indicates code within a
|
||||||
|
normal paragraph. For example:
|
||||||
|
|
||||||
|
Use the `printf()` function.
|
||||||
|
|||||||
@@ -100,7 +100,13 @@ You can pin a post both in the UI and in the backend of the server.
|
|||||||
|
|
||||||
| Status | Task |
|
| Status | Task |
|
||||||
|:---------------------------------------------:|:-------------------------------------------------:|
|
|:---------------------------------------------:|:-------------------------------------------------:|
|
||||||
|<span style="color:green;text-align:center;">DONE</span>|Code Editor in Admin Panel with saving!
|
|<span style="color:green;text-align:center;">DONE</span>|Code Editor in Admin Panel with saving!|
|
||||||
|
|<span style="color:orange;"> SEMI </span>| Exporting Tar of 'Posts/' Folder|
|
||||||
|
|
||||||
|
### <span style="color:#d42c2c;">Exporting of Folder:</span>
|
||||||
|
|
||||||
|
This for now atleast , only works with Next.JS Production Server `npm install && npm run build && npm start` for reference.
|
||||||
|
Docker Support for now is limited. I've gotten Persistence working. ( On Branch PM2 )
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ export default function AboutButton() {
|
|||||||
label="ABOUT ME"
|
label="ABOUT ME"
|
||||||
color="#2563eb"
|
color="#2563eb"
|
||||||
icon={InfoIcon}
|
icon={InfoIcon}
|
||||||
onClick={() => window.open('http://' + window.location.hostname + ':80', '_blank')}
|
onClick={() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.open('http://' + window.location.hostname + ':80', '_blank');
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function HeaderButtons() {
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
{/* If your server for about me is running on a different port, change the port number here */}
|
{/* If your server for about me is running on a different port, change the port number here */}
|
||||||
<a href={window.location.origin.replace('3000', '80')} target="_self" rel="noopener noreferrer">
|
<a href={typeof window !== 'undefined' ? window.location.origin.replace('3000', '80') : '#'} target="_self" rel="noopener noreferrer">
|
||||||
<img
|
<img
|
||||||
src="https://img.shields.io/badge/About%20Me-000000?style=for-the-badge&logo=account&logoColor=white&labelColor=2563eb"
|
src="https://img.shields.io/badge/About%20Me-000000?style=for-the-badge&logo=account&logoColor=white&labelColor=2563eb"
|
||||||
alt="About Me"
|
alt="About Me"
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export default function AdminPage() {
|
|||||||
const [changePwFeedback, setChangePwFeedback] = useState<string | null>(null);
|
const [changePwFeedback, setChangePwFeedback] = useState<string | null>(null);
|
||||||
const [previewHtml, setPreviewHtml] = useState('');
|
const [previewHtml, setPreviewHtml] = useState('');
|
||||||
const [editingPost, setEditingPost] = useState<{ slug: string, path: string } | null>(null);
|
const [editingPost, setEditingPost] = useState<{ slug: string, path: string } | null>(null);
|
||||||
|
const [isDocker, setIsDocker] = useState<boolean>(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const usernameRef = useRef<HTMLInputElement>(null);
|
const usernameRef = useRef<HTMLInputElement>(null);
|
||||||
const passwordRef = useRef<HTMLInputElement>(null);
|
const passwordRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -96,6 +97,7 @@ export default function AdminPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('pinnedPosts', JSON.stringify(pinned));
|
localStorage.setItem('pinnedPosts', JSON.stringify(pinned));
|
||||||
}, [pinned]);
|
}, [pinned]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
gfm: true,
|
gfm: true,
|
||||||
@@ -111,6 +113,14 @@ export default function AdminPage() {
|
|||||||
setPreviewHtml(marked.parse(newPost.content || '') as string);
|
setPreviewHtml(marked.parse(newPost.content || '') as string);
|
||||||
}, [newPost.content]);
|
}, [newPost.content]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if docker is used
|
||||||
|
fetch('/api/admin/docker')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => setIsDocker(!!data.docker))
|
||||||
|
.catch(() => setIsDocker(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const loadContent = async () => {
|
const loadContent = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/posts');
|
const response = await fetch('/api/posts');
|
||||||
@@ -480,6 +490,25 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function handleExportTarball() {
|
||||||
|
fetch('/api/admin/export')
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw new Error('Export failed');
|
||||||
|
const blob = await res.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'markdownblog-export.tar.gz';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
alert('Export failed: ' + err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 p-8">
|
<div className="min-h-screen bg-gray-100 p-8">
|
||||||
{pinFeedback && (
|
{pinFeedback && (
|
||||||
@@ -548,6 +577,19 @@ export default function AdminPage() {
|
|||||||
>
|
>
|
||||||
Passwort ändern
|
Passwort ändern
|
||||||
</button>
|
</button>
|
||||||
|
{/* Docker warning above export button */}
|
||||||
|
{isDocker && (
|
||||||
|
<div className="mb-2 px-4 py-2 bg-yellow-200 text-yellow-900 rounded border border-yellow-400 font-semibold text-sm text-center">
|
||||||
|
<span className="font-bold">Warning:</span> Docker is in use. Exporting will export the entire <span className="font-mono">/app</span> root directory (including all files and folders in the container's root).
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handleExportTarball}
|
||||||
|
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
|
||||||
|
title="Export the entire root folder as a tarball"
|
||||||
|
>
|
||||||
|
Export Root as Tarball
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -16,7 +17,7 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
// Construct the full path to the item
|
// Construct the full path to the item
|
||||||
const basePath = process.cwd();
|
const basePath = process.cwd();
|
||||||
const postsDir = path.join(basePath, 'posts');
|
const postsDir = getPostsDirectory();
|
||||||
|
|
||||||
// Ensure the posts directory exists
|
// Ensure the posts directory exists
|
||||||
try {
|
try {
|
||||||
|
|||||||
10
src/app/api/admin/docker/route.ts
Normal file
10
src/app/api/admin/docker/route.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { existsSync } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const rootDir = process.cwd();
|
||||||
|
const dockerDir = path.join(rootDir, 'docker');
|
||||||
|
const isDocker = existsSync(dockerDir);
|
||||||
|
return NextResponse.json({ docker: isDocker });
|
||||||
|
}
|
||||||
69
src/app/api/admin/export/route.ts
Normal file
69
src/app/api/admin/export/route.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import tar from 'tar';
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { statSync, createReadStream, existsSync } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const rootDir = process.cwd();
|
||||||
|
const dockerDir = path.join(rootDir, 'docker');
|
||||||
|
const postsDir = path.join(rootDir, 'posts');
|
||||||
|
let tarballName: string;
|
||||||
|
let tarballPath: string;
|
||||||
|
let tarCwd: string;
|
||||||
|
let tarItems: string[];
|
||||||
|
let tarOptions: any = {
|
||||||
|
gzip: true,
|
||||||
|
portable: true,
|
||||||
|
noMtime: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existsSync(dockerDir)) {
|
||||||
|
// Docker is in use: export the entire root directory (excluding node_modules, .next, etc)
|
||||||
|
tarballName = 'root-export.tar.gz';
|
||||||
|
tarballPath = path.join('/tmp', tarballName);
|
||||||
|
tarCwd = rootDir;
|
||||||
|
tarItems = ['.'];
|
||||||
|
tarOptions.file = tarballPath;
|
||||||
|
tarOptions.cwd = tarCwd;
|
||||||
|
tarOptions.filter = (filepath: string) => {
|
||||||
|
// Exclude node_modules, .next, .git, /tmp, and tarball itself
|
||||||
|
const excludes = [
|
||||||
|
'node_modules', '.next', '.git', 'tmp', 'docker.sock', tarballName
|
||||||
|
];
|
||||||
|
// Only check top-level folders/files
|
||||||
|
const rel = filepath.split(path.sep)[0];
|
||||||
|
return !excludes.includes(rel);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Not docker: export only the posts directory
|
||||||
|
tarballName = 'posts-export.tar.gz';
|
||||||
|
tarballPath = path.join('/tmp', tarballName);
|
||||||
|
tarCwd = rootDir;
|
||||||
|
tarItems = ['posts'];
|
||||||
|
tarOptions.file = tarballPath;
|
||||||
|
tarOptions.cwd = tarCwd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tarball
|
||||||
|
await tar.c(
|
||||||
|
tarOptions,
|
||||||
|
tarItems
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stream the tarball as a response
|
||||||
|
const stat = statSync(tarballPath);
|
||||||
|
const stream = createReadStream(tarballPath);
|
||||||
|
return new Response(stream as any, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/gzip',
|
||||||
|
'Content-Disposition': `attachment; filename="${tarballName}"`,
|
||||||
|
'Content-Length': stat.size.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error exporting tarball:', error);
|
||||||
|
return NextResponse.json({ error: 'Error exporting tarball' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||||
|
|
||||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
const postsDirectory = getPostsDirectory();
|
||||||
|
|
||||||
function getFolderStats(folderPath: string) {
|
function getFolderStats(folderPath: string) {
|
||||||
const fullPath = path.join(postsDirectory, folderPath);
|
const fullPath = path.join(postsDirectory, folderPath);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||||
|
|
||||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
const postsDirectory = getPostsDirectory();
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||||
|
|
||||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
const postsDirectory = getPostsDirectory();
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||||
|
|
||||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
const postsDirectory = getPostsDirectory();
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { NextResponse } from 'next/server';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import matter from 'gray-matter';
|
import matter from 'gray-matter';
|
||||||
|
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||||
|
|
||||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
const postsDirectory = getPostsDirectory();
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||||
|
|
||||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
const postsDirectory = getPostsDirectory();
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { NextResponse } from 'next/server';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import matter from 'gray-matter';
|
import matter from 'gray-matter';
|
||||||
|
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||||
|
|
||||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
const postsDirectory = getPostsDirectory();
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import { marked } from 'marked';
|
|||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
import hljs from 'highlight.js';
|
import hljs from 'highlight.js';
|
||||||
|
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||||
|
|
||||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
const postsDirectory = getPostsDirectory();
|
||||||
|
|
||||||
const renderer = new marked.Renderer();
|
const renderer = new marked.Renderer();
|
||||||
renderer.code = (code, infostring, escaped) => {
|
renderer.code = (code, infostring, escaped) => {
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import { marked } from 'marked';
|
|||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
import hljs from 'highlight.js';
|
import hljs from 'highlight.js';
|
||||||
|
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||||
|
|
||||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
const postsDirectory = getPostsDirectory();
|
||||||
|
|
||||||
const pinnedPath = path.join(postsDirectory, 'pinned.json');
|
const pinnedPath = path.join(postsDirectory, 'pinned.json');
|
||||||
let pinnedSlugs: string[] = [];
|
let pinnedSlugs: string[] = [];
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { JSDOM } from 'jsdom';
|
|||||||
import chokidar from 'chokidar';
|
import chokidar from 'chokidar';
|
||||||
import type { FSWatcher } from 'chokidar';
|
import type { FSWatcher } from 'chokidar';
|
||||||
import hljs from 'highlight.js';
|
import hljs from 'highlight.js';
|
||||||
|
import { getPostsDirectory } from './postsDirectory';
|
||||||
|
|
||||||
export interface Post {
|
export interface Post {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -19,7 +20,7 @@ export interface Post {
|
|||||||
author: string;
|
author: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
const postsDirectory = getPostsDirectory();
|
||||||
|
|
||||||
// Function to get file creation date
|
// Function to get file creation date
|
||||||
function getFileCreationDate(filePath: string): Date {
|
function getFileCreationDate(filePath: string): Date {
|
||||||
|
|||||||
9
src/lib/postsDirectory.ts
Normal file
9
src/lib/postsDirectory.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
|
||||||
|
export function getPostsDirectory() {
|
||||||
|
const rootDir = process.cwd();
|
||||||
|
const dockerDir = path.join(rootDir, 'docker');
|
||||||
|
const postsDir = path.join(rootDir, 'posts');
|
||||||
|
return existsSync(dockerDir) ? dockerDir : postsDir;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user