Fix audit form backend, update blog page, add I2P links, robots.txt, sitemap, audit form JS

- Add /api/audit endpoint to contact-api (deployed to server)
- Replace blog ghost links with 'coming soon' placeholder
- Add I2P mirror link to all page footers
- Wire up audit form frontend handler in main.js
- Update contact form handler to use fetch API
- Add robots.txt and sitemap.xml
- Add scripts/sync-eepsite.sh for I2P/clearnet sync
This commit is contained in:
BarnacleBoy 2026-04-23 22:51:21 +00:00
parent 8b0c095c50
commit 8a10eaaeb8
10 changed files with 187 additions and 66 deletions

View file

@ -39,51 +39,8 @@
<section class="section"> <section class="section">
<div class="container"> <div class="container">
<div class="content is-medium"> <div class="content is-medium has-text-centered">
<p class="has-text-grey-light mt-6">Articles coming soon. Check back later.</p>
<article class="mb-6">
<h2 class="title is-4"><a href="/blog/2026/building-multi-agent-fleet.html" class="has-text-white">Building a Multi-Agent Fleet with OpenClaw</a></h2>
<p class="has-text-grey-light mb-2">April 2026</p>
<p class="has-text-grey-light">
How I set up five specialized AI agents that work together like a small team: one researches,
one writes code, one audits quality, one hunts falsehoods, and one orchestrates. Real tasks,
real delegation, real quality control. Here's what actually works and what I wish I knew
starting out.
</p>
</article>
<article class="mb-6">
<h2 class="title is-4"><a href="/blog/2026/ai-community-manager.html" class="has-text-white">Using AI as a Community Manager Without Alienating Your Community</a></h2>
<p class="has-text-grey-light mb-2">April 2026</p>
<p class="has-text-grey-light">
I run a community platform and also build AI tools. The temptation to automate everything
is real, but communities can smell inauthenticity from miles away. Here's how I use AI
for spam detection, content research, and i18n while keeping the human touch that
actually matters.
</p>
</article>
<article class="mb-6">
<h2 class="title is-4"><a href="/blog/2026/self-hosting-ai-privacy.html" class="has-text-white">Self-Hosting AI: What You Actually Gain (and What You Don't)</a></h2>
<p class="has-text-grey-light mb-2">April 2026</p>
<p class="has-text-grey-light">
Everyone says "self-host your AI for privacy." Few explain what that actually means in
practice. I deployed an entire AI agent system on a $6/month VPS. Here's the honest
breakdown of what's private, what isn't, what costs money, and what the tradeoffs are.
</p>
</article>
<article class="mb-6">
<h2 class="title is-4"><a href="/blog/2026/i2p-ai-darknet.html" class="has-text-white">Running AI Services on I2P: A Practical Guide</a></h2>
<p class="has-text-grey-light mb-2">April 2026</p>
<p class="has-text-grey-light">
Why would anyone put an AI consulting service on the darknet? Because some clients
care about anonymity, and I2P (not Tor) is better suited for services, not just
browsing. Here's how I set up an eepsite and what I learned about serving anonymous
clients.
</p>
</article>
</div> </div>
</div> </div>
</section> </section>
@ -91,6 +48,7 @@
<footer class="footer has-background-dark has-text-light"> <footer class="footer has-background-dark has-text-light">
<div class="content has-text-centered"> <div class="content has-text-centered">
<p><strong>🌍 Krusty Planet</strong> — Privacy-focused AI consulting</p> <p><strong>🌍 Krusty Planet</strong> — Privacy-focused AI consulting</p>
<p class="is-size-7 mt-3"><span class="has-text-success">🔒 I2P Mirror:</span> <a href="http://g3xdiv7psi6y6l255kbkzowemsnd4tmbvy5ykaoajlu7oboma7zq.b32.i2p" style="color:#b5b5b5;" rel="nofollow noopener">g3xdiv7psi6y6l255kbkzowemsnd4tmbvy5ykaoajlu7oboma7zq.b32.i2p</a></p>
</div> </div>
</footer> </footer>

View file

@ -96,6 +96,7 @@
<footer class="footer has-background-dark has-text-light"> <footer class="footer has-background-dark has-text-light">
<div class="content has-text-centered"> <div class="content has-text-centered">
<p><strong>🌍 Krusty Planet</strong> — Privacy-focused AI consulting</p> <p><strong>🌍 Krusty Planet</strong> — Privacy-focused AI consulting</p>
<p class="is-size-7 mt-3"><span class="has-text-success">🔒 I2P Mirror:</span> <a href="http://g3xdiv7psi6y6l255kbkzowemsnd4tmbvy5ykaoajlu7oboma7zq.b32.i2p" style="color:#b5b5b5;" rel="nofollow noopener">g3xdiv7psi6y6l255kbkzowemsnd4tmbvy5ykaoajlu7oboma7zq.b32.i2p</a></p>
</div> </div>
</footer> </footer>

View file

@ -193,6 +193,7 @@
<footer class="footer has-background-dark has-text-light"> <footer class="footer has-background-dark has-text-light">
<div class="content has-text-centered"> <div class="content has-text-centered">
<p><strong>🌍 Krusty Planet</strong> — Privacy-focused AI consulting</p> <p><strong>🌍 Krusty Planet</strong> — Privacy-focused AI consulting</p>
<p class="is-size-7 mt-3"><span class="has-text-success">🔒 I2P Mirror:</span> <a href="http://g3xdiv7psi6y6l255kbkzowemsnd4tmbvy5ykaoajlu7oboma7zq.b32.i2p" style="color:#b5b5b5;" rel="nofollow noopener">g3xdiv7psi6y6l255kbkzowemsnd4tmbvy5ykaoajlu7oboma7zq.b32.i2p</a></p>
</div> </div>
</footer> </footer>

View file

@ -139,6 +139,7 @@
<footer class="footer has-background-dark has-text-light"> <footer class="footer has-background-dark has-text-light">
<div class="content has-text-centered"> <div class="content has-text-centered">
<p><strong>🌍 Krusty Planet</strong> — Privacy-focused AI consulting</p> <p><strong>🌍 Krusty Planet</strong> — Privacy-focused AI consulting</p>
<p class="is-size-7 mt-3"><span class="has-text-success">🔒 I2P Mirror:</span> <a href="http://g3xdiv7psi6y6l255kbkzowemsnd4tmbvy5ykaoajlu7oboma7zq.b32.i2p" style="color:#b5b5b5;" rel="nofollow noopener">g3xdiv7psi6y6l255kbkzowemsnd4tmbvy5ykaoajlu7oboma7zq.b32.i2p</a></p>
<p class="is-size-7">Anonymous. Encrypted. Professional.</p> <p class="is-size-7">Anonymous. Encrypted. Professional.</p>
</div> </div>
</footer> </footer>

View file

@ -18,13 +18,11 @@
if (btn) btn.textContent = icons[theme] || '🌙'; if (btn) btn.textContent = icons[theme] || '🌙';
} }
// Init: stored preference, then data-theme attr, then default dark
var stored = getStored(); var stored = getStored();
var current = stored || document.documentElement.getAttribute('data-theme') || 'dark'; var current = stored || document.documentElement.getAttribute('data-theme') || 'dark';
if (themes.indexOf(current) === -1) current = 'dark'; if (themes.indexOf(current) === -1) current = 'dark';
applyTheme(current); applyTheme(current);
// Toggle
document.getElementById('theme-toggle').addEventListener('click', function() { document.getElementById('theme-toggle').addEventListener('click', function() {
var idx = themes.indexOf(document.documentElement.getAttribute('data-theme')); var idx = themes.indexOf(document.documentElement.getAttribute('data-theme'));
var next = themes[(idx < 0 ? 0 : idx + 1) % themes.length]; var next = themes[(idx < 0 ? 0 : idx + 1) % themes.length];
@ -40,17 +38,16 @@ document.getElementById('nav-toggle').addEventListener('click', function() {
}); });
// Contact form - sends to backend API // Contact form - sends to backend API
var form = document.getElementById('contact-form'); var contactForm = document.getElementById('contact-form');
if (form) { if (contactForm) {
form.addEventListener('submit', function(e) { contactForm.addEventListener('submit', function(e) {
e.preventDefault(); e.preventDefault();
var status = document.getElementById('form-status'); var status = document.getElementById('form-status');
var submitBtn = form.querySelector('button[type="submit"]'); var submitBtn = contactForm.querySelector('button[type="submit"]');
var originalBtnText = submitBtn.textContent; var originalBtnText = submitBtn.textContent;
// Collect form data var formData = new FormData(contactForm);
var formData = new FormData(form);
var data = { var data = {
name: formData.get('name'), name: formData.get('name'),
email: formData.get('email'), email: formData.get('email'),
@ -58,39 +55,31 @@ if (form) {
message: formData.get('message') message: formData.get('message')
}; };
// Honeypot for spam
var honeypot = formData.get('website') || formData.get('url') || formData.get('honey'); var honeypot = formData.get('website') || formData.get('url') || formData.get('honey');
if (honeypot) { if (honeypot) {
// Bot detected - pretend success
status.className = 'notification is-success'; status.className = 'notification is-success';
status.textContent = 'Message received. We\'ll respond within 48 hours.'; status.textContent = 'Message received. We\'ll respond within 48 hours.';
status.style.display = 'block'; status.style.display = 'block';
form.reset(); contactForm.reset();
return; return;
} }
// Disable button and show sending state
submitBtn.disabled = true; submitBtn.disabled = true;
submitBtn.textContent = 'Sending...'; submitBtn.textContent = 'Sending...';
status.style.display = 'none'; status.style.display = 'none';
// Send to API
fetch('/api/contact', { fetch('/api/contact', {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json'
},
body: JSON.stringify(data) body: JSON.stringify(data)
}) })
.then(function(response) { .then(function(response) { return response.json(); })
return response.json();
})
.then(function(result) { .then(function(result) {
if (result.success) { if (result.success) {
status.className = 'notification is-success'; status.className = 'notification is-success';
status.textContent = result.message || 'Message received. We\'ll respond within 48 hours.'; status.textContent = result.message || 'Message received. We\'ll respond within 48 hours.';
status.style.display = 'block'; status.style.display = 'block';
form.reset(); contactForm.reset();
} else { } else {
throw new Error(result.error || 'Failed to send message'); throw new Error(result.error || 'Failed to send message');
} }
@ -105,4 +94,64 @@ if (form) {
submitBtn.textContent = originalBtnText; submitBtn.textContent = originalBtnText;
}); });
}); });
} }
// Audit form - sends to backend API
var auditForm = document.getElementById('audit-form');
if (auditForm) {
auditForm.addEventListener('submit', function(e) {
e.preventDefault();
var status = document.getElementById('audit-status');
var submitBtn = auditForm.querySelector('button[type="submit"]');
var originalBtnText = submitBtn.textContent;
var formData = new FormData(auditForm);
var data = {
name: formData.get('name'),
email: formData.get('email'),
business: formData.get('business'),
audit_type: formData.get('audit_type'),
description: formData.get('description')
};
var honeypot = formData.get('website') || formData.get('url') || formData.get('honey');
if (honeypot) {
status.className = 'notification is-success';
status.textContent = 'Audit request received. We\'ll be in touch within 48 hours.';
status.style.display = 'block';
auditForm.reset();
return;
}
submitBtn.disabled = true;
submitBtn.textContent = 'Submitting...';
status.style.display = 'none';
fetch('/api/audit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(function(response) { return response.json(); })
.then(function(result) {
if (result.success) {
status.className = 'notification is-success';
status.textContent = result.message || 'Audit request received. We\'ll review your situation and send a report within 48 hours.';
status.style.display = 'block';
auditForm.reset();
} else {
throw new Error(result.error || 'Failed to submit audit request');
}
})
.catch(function(error) {
status.className = 'notification is-danger';
status.textContent = error.message || 'Failed to submit audit request. Please try again.';
status.style.display = 'block';
})
.finally(function() {
submitBtn.disabled = false;
submitBtn.textContent = originalBtnText;
});
});
}

View file

@ -158,6 +158,7 @@
<footer class="footer has-background-dark has-text-light"> <footer class="footer has-background-dark has-text-light">
<div class="content has-text-centered"> <div class="content has-text-centered">
<p><strong>🌍 Krusty Planet</strong> — Privacy-focused AI consulting</p> <p><strong>🌍 Krusty Planet</strong> — Privacy-focused AI consulting</p>
<p class="is-size-7 mt-3"><span class="has-text-success">🔒 I2P Mirror:</span> <a href="http://g3xdiv7psi6y6l255kbkzowemsnd4tmbvy5ykaoajlu7oboma7zq.b32.i2p" style="color:#b5b5b5;" rel="nofollow noopener">g3xdiv7psi6y6l255kbkzowemsnd4tmbvy5ykaoajlu7oboma7zq.b32.i2p</a></p>
</div> </div>
</footer> </footer>

5
robots.txt Normal file
View file

@ -0,0 +1,5 @@
User-agent: *
Allow: /
Disallow: /api/
Sitemap: https://krustyplanet.org/sitemap.xml

71
scripts/sync-eepsite.sh Executable file
View file

@ -0,0 +1,71 @@
#!/bin/bash
# sync-eepsite.sh — Copy clearnet site to eepsite with appropriate modifications
# Usage: ./sync-eepsite.sh [--dry-run]
#
# The eepsite is a simplified version of the clearnet site:
# - No blog page (no blog content yet)
# - No free-audit page (audit form uses /api which isn't available on I2P)
# - Cross-reference links point to clearnet (not eepsite) and vice versa
# - Same JS/CSS
# - Same footer with I2P/clearnet cross-link
set -euo pipefail
CLEARNET="/var/www/krustyplanet"
EEPSITE="/var/www/eepsite"
DRY_RUN=false
if [[ "${1:-}" == "--dry-run" ]]; then
DRY_RUN=true
echo "DRY RUN — no files will be modified"
fi
# Pages that exist on both sites
SHARED_PAGES=("index.html" "services.html" "pricing.html" "contact.html")
# Pages only on clearnet (these have backend API dependencies)
CLEARNET_ONLY=("blog.html" "free-audit.html")
# Copy shared static assets
for dir in css js; do
echo "Syncing $dir/"
if $DRY_RUN; then
diff -rq "$CLEARNET/$dir/" "$EEPSITE/$dir/" || true
else
rsync -av --delete "$CLEARNET/$dir/" "$EEPSITE/$dir/"
fi
done
# Copy favicon
echo "Syncing favicon.svg"
if ! $DRY_RUN; then
cp "$CLEARNET/favicon.svg" "$EEPSITE/favicon.svg"
fi
# Sync shared pages with I2P-specific modifications
for page in "${SHARED_PAGES[@]}"; do
echo "Syncing $page"
if $DRY_RUN; then
echo " Would transform and copy $CLEARNET/$page -> $EEPSITE/$page"
else
# Start with clearnet version
cp "$CLEARNET/$page" "$EEPSITE/$page"
# Replace clearnet I2P mirror link with clearnet reference link
sed -i 's|🔒 I2P Mirror:.*b32\.i2p</a></p>|🌐 Clearnet: <a href="https://krustyplanet.org" style="color:#b5b5b5;">https://krustyplanet.org</a></p>|' "$EEPSITE/$page"
# Remove nav links to clearnet-only pages (free-audit, blog)
sed -i '/href="\/free-audit\.html"/d' "$EEPSITE/$page"
sed -i '/href="\/blog\.html"/d' "$EEPSITE/$page"
fi
done
echo "Done. Eepsite is in sync with clearnet."
echo ""
echo "NOTE: The following pages are clearnet-only (require /api backend):"
for page in "${CLEARNET_ONLY[@]}"; do
echo " - $page"
done
echo ""
echo "NOTE: The eepsite contact form JS still posts to /api/contact"
echo " which won't work over I2P. Consider adding a mailto: fallback."

View file

@ -83,6 +83,7 @@
<footer class="footer has-background-dark has-text-light"> <footer class="footer has-background-dark has-text-light">
<div class="content has-text-centered"> <div class="content has-text-centered">
<p><strong>🌍 Krusty Planet</strong> — Privacy-focused AI consulting</p> <p><strong>🌍 Krusty Planet</strong> — Privacy-focused AI consulting</p>
<p class="is-size-7 mt-3"><span class="has-text-success">🔒 I2P Mirror:</span> <a href="http://g3xdiv7psi6y6l255kbkzowemsnd4tmbvy5ykaoajlu7oboma7zq.b32.i2p" style="color:#b5b5b5;" rel="nofollow noopener">g3xdiv7psi6y6l255kbkzowemsnd4tmbvy5ykaoajlu7oboma7zq.b32.i2p</a></p>
</div> </div>
</footer> </footer>

33
sitemap.xml Normal file
View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://krustyplanet.org/</loc>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://krustyplanet.org/services.html</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://krustyplanet.org/pricing.html</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://krustyplanet.org/free-audit.html</loc>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://krustyplanet.org/contact.html</loc>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://krustyplanet.org/blog.html</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
</urlset>