diff --git a/content b/content new file mode 160000 index 0000000..aa9d99d --- /dev/null +++ b/content @@ -0,0 +1 @@ +Subproject commit aa9d99d81c3ed28f99d279e4f36c2c9ab5118aab diff --git a/package.json b/package.json index c490c7d..953572a 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "test": "echo \"Error: no test specified\" && exit 1", "start": "nodemon ./src/app.js --trace-exit", "maildev": "maildev", - "dev": "./node_modules/pm2/bin/pm2 ecosystem.config.js start --env development --watch", - "prod": "./node_modules/pm2/bin/pm2 ecosystem.config.js start --env production", + "dev": "./node_modules/pm2/bin/pm2 start --env development --watch", + "prod": "./node_modules/pm2/bin/pm2 start --env production", "stop": "node_modules/pm2/bin/pm2 delete expressjs-blog" }, "keywords": [], diff --git a/posts/.gitkeep b/posts/.gitkeep deleted file mode 100644 index e69de29..0000000 --- a/posts/.gitkeep +++ /dev/null diff --git a/posts/2025/.gitkeep b/posts/2025/.gitkeep deleted file mode 100644 index e69de29..0000000 --- a/posts/2025/.gitkeep +++ /dev/null diff --git a/posts/2025/05/.gitkeep b/posts/2025/05/.gitkeep deleted file mode 100644 index e69de29..0000000 --- a/posts/2025/05/.gitkeep +++ /dev/null diff --git a/posts/2025/05/example.md b/posts/2025/05/example.md deleted file mode 100644 index 13883b9..0000000 --- a/posts/2025/05/example.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: "Example Blog Post" -date: "2025-05-15" -author: "Jason Poage" ---- - -# Welcome to My Blog - -This is a sample blog post written in **Markdown**. - -## Features - -- Easy to write -- Converts to clean HTML -- Supports **bold**, *italic*, and `inline code` - -## Code Example - -```js -console.log("Hello, world!"); -``` diff --git a/posts/2025/06/.gitkeep b/posts/2025/06/.gitkeep deleted file mode 100644 index e69de29..0000000 --- a/posts/2025/06/.gitkeep +++ /dev/null diff --git a/public/css/footer.css b/public/css/footer.css index 3780397..261f6bb 100644 --- a/public/css/footer.css +++ b/public/css/footer.css @@ -1,7 +1,104 @@ footer { - background: #f0f0f0; - padding: 1rem 0; + background-color: #34495e; + color: #ecf0f1; + padding: 2rem 0; text-align: center; - border-top: 1px solid #ccc; + border-top: none; + box-shadow: 0 -2px 5px rgba(0,0,0,0.1); + font-size: 0.9rem; } +footer .container { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +footer p { + font-size: 0.85rem; + color: #bdc3c7; + line-height: 1.5; + margin: 0; + max-width: 600px; +} + +footer p a { + color: #9bd3ff; + text-decoration: none; + transition: color 0.2s ease-in-out; +} + +footer p a:hover { + color: #ecf0f1; + text-decoration: underline; +} + +footer img { + vertical-align: middle; + margin-left: 0.5rem; + opacity: 0.8; + transition: opacity 0.2s ease-in-out; +} + +footer img:hover { + opacity: 1; +} + + +footer p:last-of-type { + margin-top: 1rem; +} + +footer p:last-of-type a { + font-size: 1rem; + font-weight: 600; +} + +footer nav { + margin-top: 0; + margin-bottom: 0; +} + +footer nav p { + margin: 0; + +} + +footer .social-contact { + display: flex; + gap: 1rem; + flex-wrap: wrap; + justify-content: center; + margin-top: 1rem; +} + +footer .social-contact p { + margin: 0; + font-size: 1rem; + font-weight: 600; +} + +footer .social-contact p img, +footer .social-contact p svg { + vertical-align: middle; + margin: 0 0.25rem; + width: 24px; + height: 24px; + opacity: 0.9; + transition: opacity 0.2s ease-in-out; +} + +footer .social-contact p img:hover, +footer .social-contact p svg:hover { + opacity: 1; +} + +@media (max-width: 600px) { + footer .container { + padding: 1.5rem; + } + footer p { + font-size: 0.8rem; + } +} diff --git a/public/css/page.css b/public/css/page.css new file mode 100644 index 0000000..de48217 --- /dev/null +++ b/public/css/page.css @@ -0,0 +1,281 @@ + +.page-content { + max-width: 900px; + margin: 0; /* Remove auto centering since it's already in a flex container */ + padding: 3rem 2.5rem; + line-height: 1.7; + color: #2d3748; + background-color: #ffffff; + border-radius: 12px; + box-shadow: 0 4px 25px rgba(0, 0, 0, 0.06); + border: 1px solid #e2e8f0; + position: relative; +} + +.page-content::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + border-radius: 12px 12px 0 0; +} + +.page-content h1 { + font-size: 3.2rem; + color: #1a202c; + margin-bottom: 1rem; + line-height: 1.2; + text-align: center; + font-weight: 800; + letter-spacing: -0.03em; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + padding-bottom: 1.5rem; + border-bottom: 3px solid #e2e8f0; + margin-bottom: 2.5rem; +} + +.page-content h2 { + font-size: 1.9rem; + color: #2d3748; + margin-top: 3.5rem; + margin-bottom: 1.2rem; + line-height: 1.3; + font-weight: 700; + letter-spacing: -0.02em; + position: relative; + padding-left: 1rem; +} + +.page-content h2::before { + content: ''; + position: absolute; + left: 0; + top: 0.2em; + width: 4px; + height: 1.2em; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 2px; +} + +.page-content p { + font-size: 1.125rem; + margin-bottom: 1.5rem; + color: #4a5568; + text-align: justify; + hyphens: auto; +} + +.page-content h2 + p { + font-size: 1.2rem; + color: #2d3748; + font-weight: 500; + margin-bottom: 2rem; +} + + +.page-content ul { + list-style: none; + margin-left: 0; + padding-left: 0; + margin-bottom: 2rem; + space-y: 1rem; +} + +.page-content ul li { + margin-bottom: 1.5rem; + padding-left: 2rem; + position: relative; + background-color: #f8fafc; + padding: 1.5rem 1.5rem 1.5rem 3rem; + border-radius: 8px; + border-left: 4px solid #667eea; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.page-content ul li:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); +} + +.page-content ul li::before { + content: '▸'; + position: absolute; + left: 1rem; + top: 1.5rem; + color: #667eea; + font-size: 1.2rem; + font-weight: bold; +} + +.page-content ul li p { + margin-bottom: 0.8rem; + font-size: 1.1rem; + line-height: 1.6; +} + +.page-content ul li p:last-child { + margin-bottom: 0; +} + +.page-content hr { + border: none; + height: 1px; + background: linear-gradient(90deg, transparent 0%, #cbd5e0 20%, #cbd5e0 80%, transparent 100%); + margin: 3.5rem 0; + position: relative; +} + +.page-content hr::after { + content: '✦'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: #ffffff; + color: #667eea; + padding: 0 1rem; + font-size: 1.2rem; +} + +.page-content strong { + font-weight: 700; + /* color: #2d3748; */ + color: #4a5568; + /* background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); */ + /* padding: 0.1em 0.3em; */ + border-radius: 4px; +} + +.page-content strong { + font-weight: 700; /* Keeps it bold */ + /* Slightly darker than regular paragraph text for subtle emphasis */ + /* Removed background, padding, and border-radius */ +} + +.page-content a { + color: #667eea; + text-decoration: none; + font-weight: 600; + position: relative; + transition: color 0.3s ease; +} + +.page-content a::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 0; + height: 2px; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + transition: width 0.3s ease; +} + +.page-content a:hover { + color: #764ba2; +} + +.page-content a:hover::after { + width: 100%; +} + +/* Social media links special styling */ +.page-content ul li a { + display: inline-flex; + align-items: center; + padding: 0.5rem 1rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 6px; + text-decoration: none; + font-weight: 600; + transition: transform 0.2s ease, box-shadow 0.2s ease; + margin-right: 0.5rem; +} + +.page-content ul li a:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); + color: white; +} + +.page-content ul li a::after { + display: none; /* Remove the underline effect for these buttons */ +} + +@media (max-width: 900px) { + .page-content { + padding: 2rem 1.5rem; + margin: 0 1rem; + border-radius: 8px; + } + + .page-content h1 { + font-size: 2.5rem; + } + + .page-content h2 { + font-size: 1.6rem; + margin-top: 2.5rem; + } +} + +@media (max-width: 600px) { + .page-content { + padding: 1.5rem 1rem; + margin: 0 0.5rem; + border-radius: 6px; + } + + .page-content h1 { + font-size: 2rem; + padding-bottom: 1rem; + margin-bottom: 1.5rem; + } + + .page-content h2 { + font-size: 1.4rem; + margin-top: 2rem; + } + + .page-content p { + font-size: 1rem; + text-align: left; + } + + .page-content ul li { + padding: 1rem 1rem 1rem 2.5rem; + margin-bottom: 1rem; + } + + .page-content hr { + margin: 2.5rem 0; + } +} + +@media (max-width: 480px) { + .page-content { + padding: 1rem 0.8rem; + } + + .page-content h1 { + font-size: 1.8rem; + } + + .page-content h2 { + font-size: 1.3rem; + } + + .page-content ul li a { + display: block; + margin-bottom: 0.5rem; + text-align: center; + } +} diff --git a/public/css/placeholder.css b/public/css/placeholder.css new file mode 100644 index 0000000..a874c0d --- /dev/null +++ b/public/css/placeholder.css @@ -0,0 +1,50 @@ +@keyframes spin { + to { transform: rotate(360deg); } +} +p { + font-size: 1.4rem; + margin: 0.8rem auto 2rem auto; + max-width: 450px; + line-height: 1.6; + color: #666; +} + +h2 { + font-size: 2.5rem; + font-weight: 600; + color: #333; + margin-top: 1rem; + margin-bottom: 0.5rem; +} +h3 { + font-size: 1.8rem; + font-weight: 500; + color: #444; + margin-top: 0.5rem; + margin-bottom: 1rem; + line-height: 1.4; +} +.loader { + border: 6px solid #e0e0e0; + border-top: 6px solid #007acc; + border-radius: 50%; + width: 60px; + height: 60px; + animation: spin 1.2s linear infinite; + margin-top: 2rem; +} + +.placeholder { + max-width: 700px; + margin: 7rem auto; + padding: 3rem; + border: 1px solid #ddd; + background: #ffffff; + box-shadow: 0 4px 15px rgba(0,0,0,0.1); + text-align: center; + + display: flex; + flex-direction: column; + align-items: center; + gap: 1.5rem; +} diff --git a/public/css/prism.css b/public/css/prism.css new file mode 100644 index 0000000..f0eff0e --- /dev/null +++ b/public/css/prism.css @@ -0,0 +1,3 @@ +/* PrismJS 1.30.0 +https://prismjs.com/download#themes=prism&languages=markup+css+clike+javascript+bash */ +code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help} diff --git a/public/css/responsive.css b/public/css/responsive.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/public/css/responsive.css diff --git a/public/css/responsiveness.css b/public/css/responsiveness.css deleted file mode 100644 index e69de29..0000000 --- a/public/css/responsiveness.css +++ /dev/null diff --git a/public/css/site-map.css b/public/css/site-map.css new file mode 100644 index 0000000..eae67d8 --- /dev/null +++ b/public/css/site-map.css @@ -0,0 +1,53 @@ +/* Sitemap base styling */ +nav.sitemap { + max-width: 800px; + margin: 2rem auto; + padding: 1rem 2rem; + font-family: sans-serif; + background-color: #f8f9fa; + border: 1px solid #ddd; + border-radius: 8px; +} + +nav.sitemap h1 { + font-size: 1.8rem; + margin-bottom: 1rem; + text-align: center; + color: #333; +} + +/* Top-level list */ +nav.sitemap ul { + list-style-type: none; + padding-left: 0; + margin: 0; +} + +nav.sitemap li { + margin: 0.5rem 0; + position: relative; +} + +/* Links */ +nav.sitemap a { + text-decoration: none; + color: #007acc; + font-weight: 500; +} + +nav.sitemap a:hover { + text-decoration: underline; +} + +/* Nested lists */ +nav.sitemap li > ul { + margin-top: 0.3rem; + margin-left: 1.5rem; + padding-left: 1rem; + border-left: 2px solid #ddd; +} + +nav.sitemap li > ul li::before { + content: "→ "; + color: #999; +} diff --git a/public/css/tools.css b/public/css/tools.css new file mode 100644 index 0000000..af6740f --- /dev/null +++ b/public/css/tools.css @@ -0,0 +1,324 @@ +/* --- Page Content Container --- */ +.page-content { + max-width: 900px; + margin: 0; /* Remove auto centering as it's likely within a broader flex/grid layout */ + padding: 3rem 2.5rem; + line-height: 1.7; + color: #2d3748; + background-color: #ffffff; + border-radius: 12px; + box-shadow: 0 4px 25px rgba(0, 0, 0, 0.06); + border: 1px solid #e2e8f0; + position: relative; + overflow: hidden; /* Ensures content doesn't break rounded corners */ +} + +/* --- Gradient Border Top --- */ +.page-content::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + border-radius: 12px 12px 0 0; +} + +/* --- Main Page Title (H1) --- */ +.page-content h1 { + font-size: 3.2rem; + color: #1a202c; /* Fallback color */ + margin-bottom: 1rem; + line-height: 1.2; + text-align: center; + font-weight: 800; + letter-spacing: -0.03em; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + padding-bottom: 1.5rem; + border-bottom: 3px solid #e2e8f0; + margin-bottom: 2.5rem; +} + +/* --- Section Headings (H2) --- */ +.page-content h2 { + font-size: 1.9rem; + color: #2d3748; + margin-top: 3.5rem; + margin-bottom: 1.2rem; + line-height: 1.3; + font-weight: 700; + letter-spacing: -0.02em; + position: relative; + padding-left: 1rem; +} + +/* --- H2 Left Border/Accent --- */ +.page-content h2::before { + content: ''; + position: absolute; + left: 0; + top: 0.2em; + width: 4px; + height: 1.2em; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 2px; +} + +/* --- Paragraphs --- */ +.page-content p { + font-size: 1.125rem; + margin-bottom: 1.5rem; + color: #4a5568; + text-align: justify; + hyphens: auto; +} + +/* --- Paragraphs immediately following H2 --- */ +.page-content h2 + p { + font-size: 1.2rem; + color: #2d3748; + font-weight: 500; + margin-bottom: 2rem; +} + +/* --- List Styling (Main Level) --- */ +.page-content ul { + list-style: none; /* Remove default bullets */ + margin-left: 0; + padding-left: 0; + margin-bottom: 2rem; +} + +/* Apply box styling ONLY to direct children LI of ul (top-level list items) */ +.page-content ul > li { + margin-bottom: 1.5rem; + padding: 1.5rem 1.5rem 1.5rem 3rem; /* Spacing inside the box */ + position: relative; + background-color: #f8fafc; /* The 'round box' background */ + border-radius: 8px; + border-left: 4px solid #667eea; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + transition: transform 0.2s ease, box-shadow 0.2s ease; + display: block; /* Ensures proper block behavior */ + line-height: 1.6; + color: #4a5568; + font-size: 1.1rem; +} + +.page-content ul > li:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); +} + +/* Custom bullet points for main list items */ +.page-content ul > li::before { + content: '▸'; + position: absolute; + left: 1rem; + top: 1.5rem; + color: #667eea; + font-size: 1.2rem; + font-weight: bold; +} + +/* --- Nested List Styling --- */ +/* Targets any ul that is inside another ul (e.g., the System Concepts list) */ +.page-content ul ul { + list-style: disc; /* Use default disc bullets for nested lists */ + margin-top: 1rem; /* Space above the nested list */ + margin-bottom: 0.5rem; + margin-left: 2rem; /* Indent the nested list */ + padding-left: 0; /* Keep custom bullet formatting easier if you change list-style */ +} + +/* Styles for items within nested lists */ +.page-content ul ul li { + background-color: transparent; /* Remove the box background */ + border-left: none; /* Remove the left border */ + box-shadow: none; /* Remove the shadow */ + padding: 0.2rem 0; /* Simpler padding for nested items */ + margin-bottom: 0.3rem; /* Closer spacing for nested items */ + font-size: 1rem; /* Slightly smaller font for nested items */ + color: #2d3748; /* Adjust color for nested items */ + line-height: 1.5; /* Adjust line height for nested items */ + position: static; /* Remove absolute positioning if not needed for custom bullets */ +} + +/* Remove ::before for nested list items if default bullets are used */ +.page-content ul ul li::before { + content: none; +} + + +/* --- Horizontal Rule (Separator) --- */ +.page-content hr { + border: none; + height: 1px; + background: linear-gradient(90deg, transparent 0%, #cbd5e0 20%, #cbd5e0 80%, transparent 100%); + margin: 3.5rem 0; + position: relative; +} + +/* --- HR Center Icon --- */ +.page-content hr::after { + content: '✦'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: #ffffff; + color: #667eea; + padding: 0 1rem; + font-size: 1.2rem; +} + +/* --- Strong (Bold) Text --- */ +.page-content strong { + font-weight: 700; + color: #4a5568; /* Ensures bold text is readable and consistent */ +} + +/* --- Standard Links --- */ +.page-content a { + color: #667eea; + text-decoration: none; + font-weight: 600; + position: relative; + transition: color 0.3s ease; +} + +/* --- Link Hover Underline Effect --- */ +.page-content a::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 0; + height: 2px; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + transition: width 0.3s ease; +} + +.page-content a:hover { + color: #764ba2; +} + +.page-content a:hover::after { + width: 100%; +} + +/* --- Social Media Links Special Styling (Buttons) --- */ +/* This rule needs to be applied carefully. It's currently styling ALL tags + inside ul li, which might include links within your tool descriptions. + If these are only for specific social media links, consider a more specific class. */ +.page-content ul li a { + display: inline-flex; + align-items: center; + padding: 0.5rem 1rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 6px; + text-decoration: none; + font-weight: 600; + transition: transform 0.2s ease, box-shadow 0.2s ease; + margin-right: 0.5rem; + /* This overrides the standard link underline, which is likely desired here */ +} + +.page-content ul li a:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); + color: white; /* Ensure text stays white on hover for buttons */ +} + +.page-content ul li a::after { + display: none; /* Remove the standard underline effect for these buttons */ +} + +/* --- Responsive Adjustments --- */ +@media (max-width: 900px) { + .page-content { + padding: 2rem 1.5rem; + margin: 0 1rem; + border-radius: 8px; + } + + .page-content h1 { + font-size: 2.5rem; + } + + .page-content h2 { + font-size: 1.6rem; + margin-top: 2.5rem; + } +} + +@media (max-width: 600px) { + .page-content { + padding: 1.5rem 1rem; + margin: 0 0.5rem; + border-radius: 6px; + } + + .page-content h1 { + font-size: 2rem; + padding-bottom: 1rem; + margin-bottom: 1.5rem; + } + + .page-content h2 { + font-size: 1.4rem; + margin-top: 2rem; + } + + .page-content p { + font-size: 1rem; + text-align: left; /* Adjust for better readability on small screens */ + } + + .page-content ul > li { /* Target main list items */ + padding: 1rem 1rem 1rem 2.5rem; + margin-bottom: 1rem; + } + + .page-content ul > li::before { /* Adjust bullet position for smaller padding */ + top: 1rem; + left: 0.8rem; + } + + .page-content hr { + margin: 2.5rem 0; + } + + /* Nested list items on small screens */ + .page-content ul ul { + margin-left: 1.5rem; /* Adjust nested indent */ + } + .page-content ul ul li { + font-size: 0.95rem; /* Slightly smaller for hierarchy */ + } +} + +@media (max-width: 480px) { + .page-content { + padding: 1rem 0.8rem; + } + + .page-content h1 { + font-size: 1.8rem; + } + + .page-content h2 { + font-size: 1.3rem; + } + + .page-content ul li a { /* This special link styling might need a class */ + display: block; + margin-bottom: 0.5rem; + text-align: center; + } +} diff --git a/public/js/post.js b/public/js/post.js index 3a4bb5e..5df5298 100644 --- a/public/js/post.js +++ b/public/js/post.js @@ -1,3 +1,5 @@ +// static/js/post.js + // Syntax Highlighting with Prism.js (include Prism CSS/JS separately) // Automatically highlight all
 blocks
 document.querySelectorAll("pre code").forEach((block) => {
diff --git a/public/js/prism.js b/public/js/prism.js
new file mode 100644
index 0000000..9b63a44
--- /dev/null
+++ b/public/js/prism.js
@@ -0,0 +1,929 @@
+/* PrismJS 1.30.0
+https://prismjs.com/download#themes=prism&languages=markup+css+clike+javascript+bash */
+var _self =
+    "undefined" != typeof window
+      ? window
+      : "undefined" != typeof WorkerGlobalScope &&
+        self instanceof WorkerGlobalScope
+      ? self
+      : {},
+  Prism = (function (e) {
+    var n = /(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,
+      t = 0,
+      r = {},
+      a = {
+        manual: e.Prism && e.Prism.manual,
+        disableWorkerMessageHandler:
+          e.Prism && e.Prism.disableWorkerMessageHandler,
+        util: {
+          encode: function e(n) {
+            return n instanceof i
+              ? new i(n.type, e(n.content), n.alias)
+              : Array.isArray(n)
+              ? n.map(e)
+              : n
+                  .replace(/&/g, "&")
+                  .replace(/= g.reach);
+              A += w.value.length, w = w.next
+            ) {
+              var P = w.value;
+              if (n.length > e.length) return;
+              if (!(P instanceof i)) {
+                var E,
+                  S = 1;
+                if (y) {
+                  if (!(E = l(b, A, e, m)) || E.index >= e.length) break;
+                  var L = E.index,
+                    O = E.index + E[0].length,
+                    C = A;
+                  for (C += w.value.length; L >= C; )
+                    C += (w = w.next).value.length;
+                  if (((A = C -= w.value.length), w.value instanceof i))
+                    continue;
+                  for (
+                    var j = w;
+                    j !== n.tail && (C < O || "string" == typeof j.value);
+                    j = j.next
+                  )
+                    S++, (C += j.value.length);
+                  S--, (P = e.slice(A, C)), (E.index -= A);
+                } else if (!(E = l(b, 0, P, m))) continue;
+                L = E.index;
+                var N = E[0],
+                  _ = P.slice(0, L),
+                  M = P.slice(L + N.length),
+                  W = A + P.length;
+                g && W > g.reach && (g.reach = W);
+                var I = w.prev;
+                if (
+                  (_ && ((I = u(n, I, _)), (A += _.length)),
+                  c(n, I, S),
+                  (w = u(n, I, new i(f, p ? a.tokenize(N, p) : N, k, N))),
+                  M && u(n, w, M),
+                  S > 1)
+                ) {
+                  var T = { cause: f + "," + d, reach: W };
+                  o(e, n, t, w.prev, A, T),
+                    g && T.reach > g.reach && (g.reach = T.reach);
+                }
+              }
+            }
+          }
+        }
+    }
+    function s() {
+      var e = { value: null, prev: null, next: null },
+        n = { value: null, prev: e, next: null };
+      (e.next = n), (this.head = e), (this.tail = n), (this.length = 0);
+    }
+    function u(e, n, t) {
+      var r = n.next,
+        a = { value: t, prev: n, next: r };
+      return (n.next = a), (r.prev = a), e.length++, a;
+    }
+    function c(e, n, t) {
+      for (var r = n.next, a = 0; a < t && r !== e.tail; a++) r = r.next;
+      (n.next = r), (r.prev = n), (e.length -= a);
+    }
+    if (
+      ((e.Prism = a),
+      (i.stringify = function e(n, t) {
+        if ("string" == typeof n) return n;
+        if (Array.isArray(n)) {
+          var r = "";
+          return (
+            n.forEach(function (n) {
+              r += e(n, t);
+            }),
+            r
+          );
+        }
+        var i = {
+            type: n.type,
+            content: e(n.content, t),
+            tag: "span",
+            classes: ["token", n.type],
+            attributes: {},
+            language: t,
+          },
+          l = n.alias;
+        l &&
+          (Array.isArray(l)
+            ? Array.prototype.push.apply(i.classes, l)
+            : i.classes.push(l)),
+          a.hooks.run("wrap", i);
+        var o = "";
+        for (var s in i.attributes)
+          o +=
+            " " +
+            s +
+            '="' +
+            (i.attributes[s] || "").replace(/"/g, """) +
+            '"';
+        return (
+          "<" +
+          i.tag +
+          ' class="' +
+          i.classes.join(" ") +
+          '"' +
+          o +
+          ">" +
+          i.content +
+          ""
+        );
+      }),
+      !e.document)
+    )
+      return e.addEventListener
+        ? (a.disableWorkerMessageHandler ||
+            e.addEventListener(
+              "message",
+              function (n) {
+                var t = JSON.parse(n.data),
+                  r = t.language,
+                  i = t.code,
+                  l = t.immediateClose;
+                e.postMessage(a.highlight(i, a.languages[r], r)),
+                  l && e.close();
+              },
+              !1
+            ),
+          a)
+        : a;
+    var g = a.util.currentScript();
+    function f() {
+      a.manual || a.highlightAll();
+    }
+    if (
+      (g &&
+        ((a.filename = g.src),
+        g.hasAttribute("data-manual") && (a.manual = !0)),
+      !a.manual)
+    ) {
+      var h = document.readyState;
+      "loading" === h || ("interactive" === h && g && g.defer)
+        ? document.addEventListener("DOMContentLoaded", f)
+        : window.requestAnimationFrame
+        ? window.requestAnimationFrame(f)
+        : window.setTimeout(f, 16);
+    }
+    return a;
+  })(_self);
+"undefined" != typeof module && module.exports && (module.exports = Prism),
+  "undefined" != typeof global && (global.Prism = Prism);
+(Prism.languages.markup = {
+  comment: { pattern: //, greedy: !0 },
+  prolog: { pattern: /<\?[\s\S]+?\?>/, greedy: !0 },
+  doctype: {
+    pattern:
+      /"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,
+    greedy: !0,
+    inside: {
+      "internal-subset": {
+        pattern: /(^[^\[]*\[)[\s\S]+(?=\]>$)/,
+        lookbehind: !0,
+        greedy: !0,
+        inside: null,
+      },
+      string: { pattern: /"[^"]*"|'[^']*'/, greedy: !0 },
+      punctuation: /^$|[[\]]/,
+      "doctype-tag": /^DOCTYPE/i,
+      name: /[^\s<>'"]+/,
+    },
+  },
+  cdata: { pattern: //i, greedy: !0 },
+  tag: {
+    pattern:
+      /<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,
+    greedy: !0,
+    inside: {
+      tag: {
+        pattern: /^<\/?[^\s>\/]+/,
+        inside: { punctuation: /^<\/?/, namespace: /^[^\s>\/:]+:/ },
+      },
+      "special-attr": [],
+      "attr-value": {
+        pattern: /=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,
+        inside: {
+          punctuation: [
+            { pattern: /^=/, alias: "attr-equals" },
+            { pattern: /^(\s*)["']|["']$/, lookbehind: !0 },
+          ],
+        },
+      },
+      punctuation: /\/?>/,
+      "attr-name": {
+        pattern: /[^\s>\/]+/,
+        inside: { namespace: /^[^\s>\/:]+:/ },
+      },
+    },
+  },
+  entity: [
+    { pattern: /&[\da-z]{1,8};/i, alias: "named-entity" },
+    /&#x?[\da-f]{1,8};/i,
+  ],
+}),
+  (Prism.languages.markup.tag.inside["attr-value"].inside.entity =
+    Prism.languages.markup.entity),
+  (Prism.languages.markup.doctype.inside["internal-subset"].inside =
+    Prism.languages.markup),
+  Prism.hooks.add("wrap", function (a) {
+    "entity" === a.type &&
+      (a.attributes.title = a.content.replace(/&/, "&"));
+  }),
+  Object.defineProperty(Prism.languages.markup.tag, "addInlined", {
+    value: function (a, e) {
+      var s = {};
+      (s["language-" + e] = {
+        pattern: /(^$)/i,
+        lookbehind: !0,
+        inside: Prism.languages[e],
+      }),
+        (s.cdata = /^$/i);
+      var t = {
+        "included-cdata": { pattern: //i, inside: s },
+      };
+      t["language-" + e] = { pattern: /[\s\S]+/, inside: Prism.languages[e] };
+      var n = {};
+      (n[a] = {
+        pattern: RegExp(
+          "(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(
+            /__/g,
+            function () {
+              return a;
+            }
+          ),
+          "i"
+        ),
+        lookbehind: !0,
+        greedy: !0,
+        inside: t,
+      }),
+        Prism.languages.insertBefore("markup", "cdata", n);
+    },
+  }),
+  Object.defineProperty(Prism.languages.markup.tag, "addAttribute", {
+    value: function (a, e) {
+      Prism.languages.markup.tag.inside["special-attr"].push({
+        pattern: RegExp(
+          "(^|[\"'\\s])(?:" +
+            a +
+            ")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))",
+          "i"
+        ),
+        lookbehind: !0,
+        inside: {
+          "attr-name": /^[^\s=]+/,
+          "attr-value": {
+            pattern: /=[\s\S]+/,
+            inside: {
+              value: {
+                pattern: /(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,
+                lookbehind: !0,
+                alias: [e, "language-" + e],
+                inside: Prism.languages[e],
+              },
+              punctuation: [{ pattern: /^=/, alias: "attr-equals" }, /"|'/],
+            },
+          },
+        },
+      });
+    },
+  }),
+  (Prism.languages.html = Prism.languages.markup),
+  (Prism.languages.mathml = Prism.languages.markup),
+  (Prism.languages.svg = Prism.languages.markup),
+  (Prism.languages.xml = Prism.languages.extend("markup", {})),
+  (Prism.languages.ssml = Prism.languages.xml),
+  (Prism.languages.atom = Prism.languages.xml),
+  (Prism.languages.rss = Prism.languages.xml);
+!(function (s) {
+  var e =
+    /(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;
+  (s.languages.css = {
+    comment: /\/\*[\s\S]*?\*\//,
+    atrule: {
+      pattern: RegExp(
+        "@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|" + e.source + ")*?(?:;|(?=\\s*\\{))"
+      ),
+      inside: {
+        rule: /^@[\w-]+/,
+        "selector-function-argument": {
+          pattern:
+            /(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,
+          lookbehind: !0,
+          alias: "selector",
+        },
+        keyword: {
+          pattern: /(^|[^\w-])(?:and|not|only|or)(?![\w-])/,
+          lookbehind: !0,
+        },
+      },
+    },
+    url: {
+      pattern: RegExp(
+        "\\burl\\((?:" + e.source + "|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)",
+        "i"
+      ),
+      greedy: !0,
+      inside: {
+        function: /^url/i,
+        punctuation: /^\(|\)$/,
+        string: { pattern: RegExp("^" + e.source + "$"), alias: "url" },
+      },
+    },
+    selector: {
+      pattern: RegExp(
+        "(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|" +
+          e.source +
+          ")*(?=\\s*\\{)"
+      ),
+      lookbehind: !0,
+    },
+    string: { pattern: e, greedy: !0 },
+    property: {
+      pattern:
+        /(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,
+      lookbehind: !0,
+    },
+    important: /!important\b/i,
+    function: { pattern: /(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i, lookbehind: !0 },
+    punctuation: /[(){};:,]/,
+  }),
+    (s.languages.css.atrule.inside.rest = s.languages.css);
+  var t = s.languages.markup;
+  t && (t.tag.addInlined("style", "css"), t.tag.addAttribute("style", "css"));
+})(Prism);
+Prism.languages.clike = {
+  comment: [
+    { pattern: /(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/, lookbehind: !0, greedy: !0 },
+    { pattern: /(^|[^\\:])\/\/.*/, lookbehind: !0, greedy: !0 },
+  ],
+  string: {
+    pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,
+    greedy: !0,
+  },
+  "class-name": {
+    pattern:
+      /(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,
+    lookbehind: !0,
+    inside: { punctuation: /[.\\]/ },
+  },
+  keyword:
+    /\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,
+  boolean: /\b(?:false|true)\b/,
+  function: /\b\w+(?=\()/,
+  number: /\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,
+  operator: /[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,
+  punctuation: /[{}[\];(),.:]/,
+};
+(Prism.languages.javascript = Prism.languages.extend("clike", {
+  "class-name": [
+    Prism.languages.clike["class-name"],
+    {
+      pattern:
+        /(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,
+      lookbehind: !0,
+    },
+  ],
+  keyword: [
+    { pattern: /((?:^|\})\s*)catch\b/, lookbehind: !0 },
+    {
+      pattern:
+        /(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,
+      lookbehind: !0,
+    },
+  ],
+  function:
+    /#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,
+  number: {
+    pattern: RegExp(
+      "(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"
+    ),
+    lookbehind: !0,
+  },
+  operator:
+    /--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/,
+})),
+  (Prism.languages.javascript["class-name"][0].pattern =
+    /(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/),
+  Prism.languages.insertBefore("javascript", "keyword", {
+    regex: {
+      pattern: RegExp(
+        "((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"
+      ),
+      lookbehind: !0,
+      greedy: !0,
+      inside: {
+        "regex-source": {
+          pattern: /^(\/)[\s\S]+(?=\/[a-z]*$)/,
+          lookbehind: !0,
+          alias: "language-regex",
+          inside: Prism.languages.regex,
+        },
+        "regex-delimiter": /^\/|\/$/,
+        "regex-flags": /^[a-z]+$/,
+      },
+    },
+    "function-variable": {
+      pattern:
+        /#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,
+      alias: "function",
+    },
+    parameter: [
+      {
+        pattern:
+          /(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,
+        lookbehind: !0,
+        inside: Prism.languages.javascript,
+      },
+      {
+        pattern:
+          /(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,
+        lookbehind: !0,
+        inside: Prism.languages.javascript,
+      },
+      {
+        pattern:
+          /(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,
+        lookbehind: !0,
+        inside: Prism.languages.javascript,
+      },
+      {
+        pattern:
+          /((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,
+        lookbehind: !0,
+        inside: Prism.languages.javascript,
+      },
+    ],
+    constant: /\b[A-Z](?:[A-Z_]|\dx?)*\b/,
+  }),
+  Prism.languages.insertBefore("javascript", "string", {
+    hashbang: { pattern: /^#!.*/, greedy: !0, alias: "comment" },
+    "template-string": {
+      pattern:
+        /`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,
+      greedy: !0,
+      inside: {
+        "template-punctuation": { pattern: /^`|`$/, alias: "string" },
+        interpolation: {
+          pattern:
+            /((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,
+          lookbehind: !0,
+          inside: {
+            "interpolation-punctuation": {
+              pattern: /^\$\{|\}$/,
+              alias: "punctuation",
+            },
+            rest: Prism.languages.javascript,
+          },
+        },
+        string: /[\s\S]+/,
+      },
+    },
+    "string-property": {
+      pattern:
+        /((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,
+      lookbehind: !0,
+      greedy: !0,
+      alias: "property",
+    },
+  }),
+  Prism.languages.insertBefore("javascript", "operator", {
+    "literal-property": {
+      pattern:
+        /((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,
+      lookbehind: !0,
+      alias: "property",
+    },
+  }),
+  Prism.languages.markup &&
+    (Prism.languages.markup.tag.addInlined("script", "javascript"),
+    Prism.languages.markup.tag.addAttribute(
+      "on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)",
+      "javascript"
+    )),
+  (Prism.languages.js = Prism.languages.javascript);
+!(function (e) {
+  var t =
+      "\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",
+    a = {
+      pattern: /(^(["']?)\w+\2)[ \t]+\S.*/,
+      lookbehind: !0,
+      alias: "punctuation",
+      inside: null,
+    },
+    n = {
+      bash: a,
+      environment: { pattern: RegExp("\\$" + t), alias: "constant" },
+      variable: [
+        {
+          pattern: /\$?\(\([\s\S]+?\)\)/,
+          greedy: !0,
+          inside: {
+            variable: [
+              { pattern: /(^\$\(\([\s\S]+)\)\)/, lookbehind: !0 },
+              /^\$\(\(/,
+            ],
+            number:
+              /\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee]-?\d+)?/,
+            operator:
+              /--|\+\+|\*\*=?|<<=?|>>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,
+            punctuation: /\(\(?|\)\)?|,|;/,
+          },
+        },
+        {
+          pattern: /\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,
+          greedy: !0,
+          inside: { variable: /^\$\(|^`|\)$|`$/ },
+        },
+        {
+          pattern: /\$\{[^}]+\}/,
+          greedy: !0,
+          inside: {
+            operator: /:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,
+            punctuation: /[\[\]]/,
+            environment: {
+              pattern: RegExp("(\\{)" + t),
+              lookbehind: !0,
+              alias: "constant",
+            },
+          },
+        },
+        /\$(?:\w+|[#?*!@$])/,
+      ],
+      entity:
+        /\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/,
+    };
+  (e.languages.bash = {
+    shebang: { pattern: /^#!\s*\/.*/, alias: "important" },
+    comment: { pattern: /(^|[^"{\\$])#.*/, lookbehind: !0 },
+    "function-name": [
+      {
+        pattern: /(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,
+        lookbehind: !0,
+        alias: "function",
+      },
+      { pattern: /\b[\w-]+(?=\s*\(\s*\)\s*\{)/, alias: "function" },
+    ],
+    "for-or-select": {
+      pattern: /(\b(?:for|select)\s+)\w+(?=\s+in\s)/,
+      alias: "variable",
+      lookbehind: !0,
+    },
+    "assign-left": {
+      pattern: /(^|[\s;|&]|[<>]\()\w+(?:\.\w+)*(?=\+?=)/,
+      inside: {
+        environment: {
+          pattern: RegExp("(^|[\\s;|&]|[<>]\\()" + t),
+          lookbehind: !0,
+          alias: "constant",
+        },
+      },
+      alias: "variable",
+      lookbehind: !0,
+    },
+    parameter: {
+      pattern: /(^|\s)-{1,2}(?:\w+:[+-]?)?\w+(?:\.\w+)*(?=[=\s]|$)/,
+      alias: "variable",
+      lookbehind: !0,
+    },
+    string: [
+      {
+        pattern: /((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,
+        lookbehind: !0,
+        greedy: !0,
+        inside: n,
+      },
+      {
+        pattern: /((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,
+        lookbehind: !0,
+        greedy: !0,
+        inside: { bash: a },
+      },
+      {
+        pattern:
+          /(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,
+        lookbehind: !0,
+        greedy: !0,
+        inside: n,
+      },
+      { pattern: /(^|[^$\\])'[^']*'/, lookbehind: !0, greedy: !0 },
+      {
+        pattern: /\$'(?:[^'\\]|\\[\s\S])*'/,
+        greedy: !0,
+        inside: { entity: n.entity },
+      },
+    ],
+    environment: { pattern: RegExp("\\$?" + t), alias: "constant" },
+    variable: n.variable,
+    function: {
+      pattern:
+        /(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,
+      lookbehind: !0,
+    },
+    keyword: {
+      pattern:
+        /(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,
+      lookbehind: !0,
+    },
+    builtin: {
+      pattern:
+        /(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,
+      lookbehind: !0,
+      alias: "class-name",
+    },
+    boolean: {
+      pattern: /(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,
+      lookbehind: !0,
+    },
+    "file-descriptor": { pattern: /\B&\d\b/, alias: "important" },
+    operator: {
+      pattern:
+        /\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,
+      inside: { "file-descriptor": { pattern: /^\d/, alias: "important" } },
+    },
+    punctuation: /\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,
+    number: { pattern: /(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/, lookbehind: !0 },
+  }),
+    (a.inside = e.languages.bash);
+  for (
+    var s = [
+        "comment",
+        "function-name",
+        "for-or-select",
+        "assign-left",
+        "parameter",
+        "string",
+        "environment",
+        "function",
+        "keyword",
+        "builtin",
+        "boolean",
+        "file-descriptor",
+        "operator",
+        "punctuation",
+        "number",
+      ],
+      o = n.variable[1].inside,
+      i = 0;
+    i < s.length;
+    i++
+  )
+    o[s[i]] = e.languages.bash[s[i]];
+  (e.languages.sh = e.languages.bash), (e.languages.shell = e.languages.bash);
+})(Prism);
diff --git a/src/routes/about.js b/src/routes/about.js
index a1acd7f..2547c96 100644
--- a/src/routes/about.js
+++ b/src/routes/about.js
@@ -1,13 +1,37 @@
 // src/routes/about.js
 const express = require("express");
 const router = express.Router();
+
+const { marked } = require("marked");
+const fs = require("fs").promises;
+const path = require("path");
+const matter = require("gray-matter");
+
 const getBaseContext = require("../utils/baseContext");
 
-router.get("/about", async (req, res) => {
-  const context = await getBaseContext({
-    title: "About",
-  });
-  res.render("pages/about.handlebars", context);
-});
+// router.get("/about", async (req, res) => {
+//   const context = await getBaseContext({
+//     title: "About",
+//   });
+//   res.render("pages/about.handlebars", context);
+// });
 
+router.get("/about", async (req, res, next) => {
+  try {
+    const aboutPath = path.join(__dirname, "../../content/pages/about.md");
+    const fileContent = await fs.readFile(aboutPath, "utf8");
+    const { data: frontmatter, content } = matter(fileContent);
+    const htmlContent = marked(content);
+    const context = await getBaseContext({
+      title: frontmatter.title,
+      author: frontmatter.author,
+      date: frontmatter.date,
+      content: htmlContent,
+    });
+    res.render("pages/page", context);
+  } catch (err) {
+    err.statusCode = 500;
+    next(err);
+  }
+});
 module.exports = router;
diff --git a/src/routes/construction.js b/src/routes/construction.js
new file mode 100644
index 0000000..284fde9
--- /dev/null
+++ b/src/routes/construction.js
@@ -0,0 +1,60 @@
+// src/routes/construction.js
+const express = require("express");
+const router = express.Router();
+const getBaseContext = require("../utils/baseContext");
+
+// const construction = async (req, res) => {
+//   const context = await getBaseContext({
+//     title: "Page Under Construction",
+//   });
+//   res.render("pages/construction.handlebars", context);
+// };
+
+const construction = async (path, title) => {
+  router.get(path, async (req, res) => {
+    const context = await getBaseContext({
+      title,
+    });
+    res.render("pages/construction.handlebars", context);
+  });
+};
+const fs = require("fs/promises");
+const path = require("path");
+const matter = require("gray-matter");
+const { marked } = require("marked");
+
+const page = async (routePath, pageFile) => {
+  router.get(routePath, async (req, res, next) => {
+    try {
+      const filePath = path.join(
+        __dirname,
+        `../../content/pages/${pageFile}.md`
+      );
+      const fileContent = await fs.readFile(filePath, "utf8");
+      const { data: frontmatter, content } = matter(fileContent);
+      const htmlContent = marked(content);
+
+      const context = await getBaseContext({
+        title: frontmatter.title,
+        content: htmlContent,
+      });
+
+      res.render(`pages/${pageFile}`, context);
+    } catch (err) {
+      err.statusCode = 500;
+      next(err);
+    }
+  });
+};
+
+construction("/newsletter", "Newsletter");
+construction("/changelog", "Changelog");
+construction("/archive", "Archive");
+construction("/rss-feed.xml", "RSS Feed");
+construction("/tags", "Tags");
+construction("/blog", "Blog");
+
+page("/projects", "projects", "Projects");
+page("/tools", "tools", "Tools");
+
+module.exports = router;
diff --git a/src/routes/contact.js b/src/routes/contact.js
index a70416f..a15699f 100644
--- a/src/routes/contact.js
+++ b/src/routes/contact.js
@@ -13,12 +13,14 @@
     next(err);
   }
 });
+
 router.get("/contact", async (req, res) => {
   const context = await getBaseContext({
     title: "Contact",
   });
   res.render("pages/contact.handlebars", context);
 });
+
 router.get("/contact/thankyou", async (req, res) => {
   const context = await getBaseContext({
     title: "Thank You",
diff --git a/src/routes/index.js b/src/routes/index.js
index 38539cd..8d3523c 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -1,15 +1,23 @@
 // src/routes/index.js
 const express = require("express");
 const router = express.Router();
-const contact = require("./contact");
-const about = require("./about");
-const post = require("./post");
 
 const getBaseContext = require("../utils/baseContext");
 
+const contact = require("./contact");
+const about = require("./about");
+const site_map = require("./site-map");
+const post = require("./post");
+const construction = require("./construction");
+
 router.use(contact);
 router.use(about);
+router.use(site_map);
+
 router.get("/post/:year/:month/:name", post);
+
+router.use(construction);
+
 router.get("/", async (req, res) => {
   const context = await getBaseContext({
     title: "Blog Home",
diff --git a/src/routes/post.js b/src/routes/post.js
index 86151ac..2eb52e3 100644
--- a/src/routes/post.js
+++ b/src/routes/post.js
@@ -1,6 +1,4 @@
-// src/routes/index.js
-const express = require("express");
-const router = express.Router();
+// src/routes/post.js
 const { marked } = require("marked");
 const fs = require("fs").promises;
 const path = require("path");
@@ -32,7 +30,13 @@
     return next(error);
   }
 
-  const mdPath = path.join(__dirname, "../../posts", year, month, `${name}.md`);
+  const mdPath = path.join(
+    __dirname,
+    "../../content/posts",
+    year,
+    month,
+    `${name}.md`
+  );
 
   try {
     const fileContent = await fs.readFile(mdPath, "utf8");
diff --git a/src/routes/site-map.js b/src/routes/site-map.js
new file mode 100644
index 0000000..81931f7
--- /dev/null
+++ b/src/routes/site-map.js
@@ -0,0 +1,13 @@
+// src/routes/site-map.js
+const express = require("express");
+const router = express.Router();
+const getBaseContext = require("../utils/baseContext");
+
+router.get("/site-map", async (req, res) => {
+  const context = await getBaseContext({
+    title: "Site Map",
+  });
+  res.render("pages/site-map.handlebars", context);
+});
+
+module.exports = router;
diff --git a/src/utils/baseContext.js b/src/utils/baseContext.js
index 694f87f..4daef11 100644
--- a/src/utils/baseContext.js
+++ b/src/utils/baseContext.js
@@ -4,7 +4,7 @@
 const { formatMonth } = require("../utils/formatMonth");
 
 async function getBaseContext(overrides = {}) {
-  const menu = await getPostsMenu(path.join(__dirname, "../../posts"));
+  const menu = await getPostsMenu(path.join(__dirname, "../../content/posts"));
   return Object.assign(
     {
       siteOwner: process.env.SITE_OWNER,
@@ -12,6 +12,9 @@
       navLinks: [
         { href: "/", label: "Home" },
         { href: "/about", label: "About" },
+        { href: "/newsletter", label: "Newsletter" },
+        { href: "/tools", label: "Tools I use" },
+        { href: "/projects", label: "Projects" },
         { href: "/contact", label: "Contact" },
       ],
       years: menu,
diff --git a/src/utils/mailer.js b/src/utils/mailer.js
index 47a0299..a9178e1 100644
--- a/src/utils/mailer.js
+++ b/src/utils/mailer.js
@@ -17,13 +17,13 @@
   auth,
 });
 
-function sendContactMail({ name, email, message }) {
+function sendContactMail({ name, email, subject, message }) {
   const { DOMAIN: domain } = process.env;
   const data = {
     from: `"Contact Form" `,
     to: process.env.MAIL_USER,
     replyTo: `"${name}" <${email}>`,
-    subject: "New Contact Form Submission",
+    subject: subject || "New Contact Form Submission",
     text: message,
   };
   console.log(data);
diff --git a/src/views/layouts/main.handlebars b/src/views/layouts/main.handlebars
index abe308f..027e646 100644
--- a/src/views/layouts/main.handlebars
+++ b/src/views/layouts/main.handlebars
@@ -4,6 +4,7 @@
 
   
   
+  
   
   {{{_sections.styles}}}
   {{title}}
diff --git a/src/views/pages/about.handlebars b/src/views/pages/about.handlebars
index bd84b80..595b430 100644
--- a/src/views/pages/about.handlebars
+++ b/src/views/pages/about.handlebars
@@ -1,8 +1,6 @@
-
 

About

This blog is maintained by Jason Poage. It covers programming, technology, and related topics.

- diff --git a/src/views/pages/construction.handlebars b/src/views/pages/construction.handlebars new file mode 100644 index 0000000..a266706 --- /dev/null +++ b/src/views/pages/construction.handlebars @@ -0,0 +1,9 @@ +{{#section "styles"}} + +{{/section}} +
+

{{title}}

+

Page Under Construction

+

This page is a placeholder. Content will be added soon.

+
+
diff --git a/src/views/pages/contact.handlebars b/src/views/pages/contact.handlebars index 2b35c2f..00ef036 100644 --- a/src/views/pages/contact.handlebars +++ b/src/views/pages/contact.handlebars @@ -8,10 +8,14 @@ -
- + +
+
+ +
diff --git a/src/views/pages/home.handlebars b/src/views/pages/home.handlebars index e37f90f..a4ec0e0 100644 --- a/src/views/pages/home.handlebars +++ b/src/views/pages/home.handlebars @@ -5,3 +5,12 @@

{{content}}

+ + + diff --git a/src/views/pages/page.handlebars b/src/views/pages/page.handlebars new file mode 100644 index 0000000..0684d5a --- /dev/null +++ b/src/views/pages/page.handlebars @@ -0,0 +1,6 @@ +{{#section "styles"}} + +{{/section}} +
+ {{{content}}} +
diff --git a/src/views/pages/post.handlebars b/src/views/pages/post.handlebars index f64a83d..974edf1 100644 --- a/src/views/pages/post.handlebars +++ b/src/views/pages/post.handlebars @@ -5,5 +5,6 @@ {{{content}}}
{{#section "scripts"}} + {{/section}} diff --git a/src/views/pages/site-map.handlebars b/src/views/pages/site-map.handlebars new file mode 100644 index 0000000..e8f43be --- /dev/null +++ b/src/views/pages/site-map.handlebars @@ -0,0 +1,26 @@ +{{#section "styles"}} + +{{/section}} + +
diff --git a/src/views/pages/tools.handlebars b/src/views/pages/tools.handlebars new file mode 100644 index 0000000..f61dbb5 --- /dev/null +++ b/src/views/pages/tools.handlebars @@ -0,0 +1,6 @@ +{{#section "styles"}} + +{{/section}} +
+ {{{content}}} +
diff --git a/src/views/partials/footer.handlebars b/src/views/partials/footer.handlebars index 6ea9b16..c693c9d 100644 --- a/src/views/partials/footer.handlebars +++ b/src/views/partials/footer.handlebars @@ -1,10 +1,32 @@ +
+

+ To the extent possible under law, {{siteOwner}} has waived all copyright and related or neighboring rights to this + work. + + CC0 + +

-
+ + + +