diff --git a/www/public/js/params.js b/www/public/js/params.js
new file mode 100644
index 0000000..12de2e1
--- /dev/null
+++ b/www/public/js/params.js
@@ -0,0 +1,45 @@
+!function() {
+ var p = new URLSearchParams(location.search);
+
+ var just = p.get('just');
+ if (just && /^[a-z0-9-]+$/.test(just)) {
+ document.documentElement.dataset.just = just;
+ var css = 'section[data-section]:not([data-section="' + just + '"]){display:none}'
+ + ' section[data-section="' + just + '"] .section-label{pointer-events:none;text-decoration:none;color:inherit}';
+ document.head.appendChild(Object.assign(document.createElement('style'), { textContent: css }));
+ }
+
+ var act = p.get('do');
+ var urls = window.__urls;
+ if (urls && urls.length) {
+ if (act === 'random') {
+ var url = urls[Math.floor(Math.random() * urls.length)];
+ location.replace(url.startsWith('http') ? url : location.origin + url);
+ }
+ if (act === 'newest') {
+ location.replace(urls[0].startsWith('http') ? urls[0] : location.origin + urls[0]);
+ }
+ }
+ if (act === 'admin') {
+ location.replace('/admin');
+ }
+
+ var has = p.get('has');
+ if (has) {
+ document.documentElement.dataset.has = has;
+ has = has.toLowerCase();
+ document.addEventListener('DOMContentLoaded', function() {
+ document.querySelectorAll('section[data-section] pre').forEach(function(pre) {
+ var lines = pre.innerHTML.split('\n');
+ pre.innerHTML = lines.filter(function(line) {
+ return !line.trim() || line.toLowerCase().indexOf(has) !== -1;
+ }).join('\n');
+ });
+ document.querySelectorAll('.guestbook-entry').forEach(function(entry) {
+ if (entry.textContent.toLowerCase().indexOf(has) === -1) {
+ entry.style.display = 'none';
+ }
+ });
+ });
+ }
+}();
diff --git a/www/src/layouts/Layout.astro b/www/src/layouts/Layout.astro
index a09d6ef..36a569b 100644
--- a/www/src/layouts/Layout.astro
+++ b/www/src/layouts/Layout.astro
@@ -5,17 +5,25 @@ interface Props {
title: string;
showHeader?: boolean;
isHome?: boolean;
+ urls?: string[];
}
-const { title, showHeader = true, isHome = false } = Astro.props;
+const { title, showHeader = true, isHome = false, urls = [] } = Astro.props;
---
-
{title}
+
+
+
+ {title}
+
+ {urls.length > 0 && }
+
+
{showHeader && (
)}
diff --git a/www/src/lib/rate-limit.ts b/www/src/lib/rate-limit.ts
new file mode 100644
index 0000000..6f51ca5
--- /dev/null
+++ b/www/src/lib/rate-limit.ts
@@ -0,0 +1,15 @@
+const requests = new Map();
+
+export function isRateLimited(key: string, maxRequests: number, windowMs: number): boolean {
+ const now = Date.now();
+ const timestamps = requests.get(key) ?? [];
+ const recent = timestamps.filter(t => now - t < windowMs);
+
+ if (recent.length >= maxRequests) {
+ return true;
+ }
+
+ recent.push(now);
+ requests.set(key, recent);
+ return false;
+}
diff --git a/www/src/pages/[slug].astro b/www/src/pages/[slug].astro
index c8c281e..caeea05 100644
--- a/www/src/pages/[slug].astro
+++ b/www/src/pages/[slug].astro
@@ -25,9 +25,9 @@ const related = post.data.related ? resolveRelatedPosts(post.data.related, allPo
{related.length > 0 && (
-
- related
+
+ related
formatListItem(p.dates.created, `/${getSlug(p.id)}`, p.data.title)).join('\n')} />
-
+
)}
diff --git a/www/src/pages/api/guestbook.ts b/www/src/pages/api/guestbook.ts
index 79a2d04..962ebf5 100644
--- a/www/src/pages/api/guestbook.ts
+++ b/www/src/pages/api/guestbook.ts
@@ -1,11 +1,17 @@
import type { APIRoute } from 'astro';
import { createEntry } from '../../lib/db';
import { jsonResponse, errorResponse } from '../../lib/api';
+import { isRateLimited } from '../../lib/rate-limit';
export const prerender = false;
export const POST: APIRoute = async ({ request }) => {
try {
+ const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown';
+ if (isRateLimited(ip, 3, 60_000)) {
+ return errorResponse('Too many requests, try again later', 429);
+ }
+
const data = await request.json();
const { name, message, url } = data;
diff --git a/www/src/pages/bookmarks/index.astro b/www/src/pages/bookmarks/index.astro
deleted file mode 100644
index 73f8438..0000000
--- a/www/src/pages/bookmarks/index.astro
+++ /dev/null
@@ -1,16 +0,0 @@
----
-import { getCollection } from 'astro:content';
-import Layout from '../../layouts/Layout.astro';
-import { formatListItem, extractDomain } from '../../lib/format';
-
-const bookmarksCollection = await getCollection('bookmarks');
-const bookmarks = bookmarksCollection
- .sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
----
-
-
-
- bookmarks
- formatListItem(b.data.date, b.data.url, b.data.title, { suffix: `(${extractDomain(b.data.url)})` })).join('\n')} />
-
-
diff --git a/www/src/pages/guestbook/index.astro b/www/src/pages/guestbook/index.astro
deleted file mode 100644
index 92ecdbf..0000000
--- a/www/src/pages/guestbook/index.astro
+++ /dev/null
@@ -1,35 +0,0 @@
----
-import Layout from '../../layouts/Layout.astro';
-import { getApprovedEntries, type GuestbookEntry } from '../../lib/db';
-import { formatDate } from '../../lib/format';
-
-let guestbookEntries: GuestbookEntry[] = [];
-try {
- guestbookEntries = await getApprovedEntries();
-} catch {
- // DB not available during dev without env vars
-}
----
-
-
-
- guestbook
-
- {guestbookEntries.map(e => (
-
- {formatDate(e.createdAt)}
- ${e.name}` : e.name} /> {e.message}
-
- ))}
-
-
-
-
-
-
diff --git a/www/src/pages/index.astro b/www/src/pages/index.astro
index 00eeea0..19c56fb 100644
--- a/www/src/pages/index.astro
+++ b/www/src/pages/index.astro
@@ -22,56 +22,57 @@ try {
} catch {
// DB not available during dev without env vars
}
+
+const urls = [
+ ...posts.map(post => ({ url: `/${getSlug(post.id)}`, date: post.dates.created.getTime() })),
+ ...txtFiles.map(f => ({ url: `/${f.name}`, date: f.date.getTime() })),
+ ...bookmarksCollection.map(b => ({ url: b.data.url, date: b.data.date.getTime() })),
+].sort((a, b) => b.date - a.date).map(e => e.url);
---
-
+
{sortedCategories.map(category => {
const categoryPosts = grouped[category];
return (
-
- {category}
- formatListItem(post.dates.created, `/${getSlug(post.id)}`, post.data.title, { pinned: post.data.pinned })),
- ...(categoryPosts.length > 10 ? [`+${categoryPosts.length - 10} more`] : [])
-].join('\n')} />
-
+
+ {category}
+ formatListItem(post.dates.created, `/${getSlug(post.id)}`, post.data.title, { pinned: post.data.pinned })).join('\n')} />
+
);
})}
-
- txt
- formatListItem(f.date, `/${f.name}`, f.name, { pinned: f.pinned })),
- ...(txtFiles.length > 10 ? [`+${txtFiles.length - 10} more`] : [])
-].join('\n')} />
-
+
+ txt
+ formatListItem(f.date, `/${f.name}`, f.name, { pinned: f.pinned })).join('\n')} />
+
-
- bookmarks
- formatListItem(b.data.date, b.data.url, b.data.title, { suffix: `(${extractDomain(b.data.url)})` })),
- ...(bookmarks.length > 10 ? [`+${bookmarks.length - 10} more`] : [])
-].join('\n')} />
-
+
+ bookmarks
+ formatListItem(b.data.date, b.data.url, b.data.title, { suffix: `(${extractDomain(b.data.url)})` })).join('\n')} />
+
-
- guestbook
+
+
+
diff --git a/www/src/pages/md/index.astro b/www/src/pages/md/index.astro
deleted file mode 100644
index fc3b0e3..0000000
--- a/www/src/pages/md/index.astro
+++ /dev/null
@@ -1,19 +0,0 @@
----
-import { getCollection } from 'astro:content';
-import Layout from '../../layouts/Layout.astro';
-import { formatListItem } from '../../lib/format';
-import { organizePostsByCategory, getSlug, enrichPostsWithDates } from '../../lib/md';
-
-const rawPosts = await getCollection('md');
-const posts = enrichPostsWithDates(rawPosts);
-const { grouped, categories: sortedCategories } = organizePostsByCategory(posts);
----
-
-
-{sortedCategories.map(category => (
-
- {category}
- formatListItem(post.dates.created, `/${getSlug(post.id)}`, post.data.title, { pinned: post.data.pinned })).join('\n')} />
-
-))}
-
diff --git a/www/src/pages/random.ts b/www/src/pages/random.ts
deleted file mode 100644
index ab17428..0000000
--- a/www/src/pages/random.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { getCollection } from 'astro:content';
-import type { APIContext } from 'astro';
-import { getSlug } from '../lib/md';
-import { getTxtFileNames } from '../lib/txt';
-
-export const prerender = false;
-
-export async function GET(context: APIContext) {
- const site = context.site?.origin ?? 'https://wynne.rs';
- const posts = await getCollection('md');
- const bookmarks = await getCollection('bookmarks');
- const txtFiles = getTxtFileNames();
-
- const urls = [
- ...posts.map(post => `/${getSlug(post.id)}`),
- ...txtFiles.map(txt => `/${txt}`),
- ...bookmarks.map(b => b.data.url),
- ];
-
- const random = urls[Math.floor(Math.random() * urls.length)];
- const redirectUrl = random.startsWith('http') ? random : `${site}${random}`;
-
- return Response.redirect(redirectUrl, 302);
-}
diff --git a/www/src/pages/sitemap.txt.ts b/www/src/pages/sitemap.txt.ts
index af030c5..464b8ad 100644
--- a/www/src/pages/sitemap.txt.ts
+++ b/www/src/pages/sitemap.txt.ts
@@ -15,10 +15,7 @@ export async function GET(context: APIContext) {
const urls = [
'/',
...posts.map(post => `/${getSlug(post.id)}`),
- '/txt',
...txtFiles.map(txt => `/${txt}`),
- '/bookmarks',
- '/guestbook',
].map(p => `${site}${p}`);
return new Response([...urls, ...SUBDOMAINS].join('\n'), {
diff --git a/www/src/pages/txt/index.astro b/www/src/pages/txt/index.astro
deleted file mode 100644
index 4b55aa7..0000000
--- a/www/src/pages/txt/index.astro
+++ /dev/null
@@ -1,14 +0,0 @@
----
-import Layout from '../../layouts/Layout.astro';
-import { formatListItem } from '../../lib/format';
-import { getTxtFiles } from '../../lib/txt';
-
-const txtFiles = getTxtFiles();
----
-
-
-
- txt
- formatListItem(f.date, `/${f.name}`, f.name, { pinned: f.pinned })).join('\n')} />
-
-
diff --git a/www/src/scripts/guestbook-sign.ts b/www/src/scripts/guestbook-sign.ts
index b9cdf47..b04befb 100644
--- a/www/src/scripts/guestbook-sign.ts
+++ b/www/src/scripts/guestbook-sign.ts
@@ -1,30 +1,42 @@
-export function initGuestbookSign() {
- document.getElementById('sign-guestbook')?.addEventListener('click', async (e) => {
+export function initGuestbookForm() {
+ const form = document.getElementById('guestbook-form') as HTMLFormElement | null;
+ if (!form) return;
+
+ const status = document.getElementById('guestbook-status')!;
+
+ form.addEventListener('submit', async (e) => {
e.preventDefault();
- const status = document.getElementById('guestbook-status')!;
+ status.textContent = '';
- const name = prompt('name:');
- if (!name) return;
+ const data = new FormData(form);
+ const name = (data.get('name') as string).trim();
+ const message = (data.get('message') as string).trim();
+ const url = (data.get('url') as string).trim() || null;
- const message = prompt('message:');
- if (!message) return;
+ if (!name || !message) return;
- const url = prompt('url (optional):');
+ const button = form.querySelector('button')!;
+ button.disabled = true;
try {
const res = await fetch('/api/guestbook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ name, message, url: url || null }),
+ body: JSON.stringify({ name, message, url }),
});
if (res.ok) {
status.textContent = ' thanks! pending approval.';
+ form.reset();
+ } else if (res.status === 429) {
+ status.textContent = ' too many requests, try later.';
} else {
status.textContent = ' error';
}
} catch {
status.textContent = ' failed';
+ } finally {
+ button.disabled = false;
}
});
}
diff --git a/www/src/styles/global.css b/www/src/styles/global.css
index 8dfeead..3191ef4 100644
--- a/www/src/styles/global.css
+++ b/www/src/styles/global.css
@@ -72,21 +72,31 @@ div.grid .content {
display: block;
}
-details {
+section {
margin: 1rem 0;
}
-summary {
- cursor: pointer;
+section .section-label {
font-family: monospace;
- list-style: none;
}
-summary::-webkit-details-marker {
+.home-name-link {
display: none;
}
-details pre {
+html[data-just] .home-name {
+ display: none;
+}
+
+html[data-just] .home-name-link {
+ display: inline;
+}
+
+html[data-has] .guestbook-form {
+ display: none;
+}
+
+section pre {
margin: 0;
}
@@ -99,3 +109,9 @@ details pre {
grid-template-columns: 8ch 1fr;
gap: 0 4ch;
}
+
+.guestbook-form {
+ margin-top: 0.5rem;
+ margin-left: 12ch;
+ font-family: monospace;
+}