- Replaced global navbar, now mobile friendly
- Corrected sizing of LyricSearchInputField - Removed sitemap from auto-generated robots.txt
This commit is contained in:
@@ -247,7 +247,8 @@ export function LyricSearchInputField({ id, placeholder, setShowLyrics }) {
|
||||
onShow={handlePanelShow}
|
||||
placeholder={placeholder}
|
||||
autoFocus
|
||||
size={40}
|
||||
style={{ width: '100%', maxWidth: '900px' }}
|
||||
inputStyle={{ width: '100%' }}
|
||||
aria-controls="lyric-search-input"
|
||||
/>
|
||||
<Button onClick={() => handleSearch()} className="btn">
|
||||
|
||||
@@ -13,85 +13,264 @@ const navItems = [
|
||||
// { label: "Git", href: "https://kode.boatson.boats", icon: ExitToApp },
|
||||
];
|
||||
|
||||
|
||||
const currentPath = Astro.url.pathname;
|
||||
---
|
||||
|
||||
<script is:inline>
|
||||
toggleTheme = () => {
|
||||
const currentTheme = document.documentElement.getAttribute("data-theme");
|
||||
const newTheme = currentTheme === "dark" ? "light" : "dark";
|
||||
document.dispatchEvent(new CustomEvent("set-theme", { detail: newTheme }));
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile menu toggle
|
||||
function initMobileMenu() {
|
||||
const menuBtn = document.getElementById('mobile-menu-btn');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const menuIcon = document.getElementById('menu-icon');
|
||||
const closeIcon = document.getElementById('close-icon');
|
||||
|
||||
if (!menuBtn || !mobileMenu || !menuIcon || !closeIcon) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove existing listeners by cloning (prevents duplicate listeners)
|
||||
const newMenuBtn = menuBtn.cloneNode(true);
|
||||
menuBtn.parentNode.replaceChild(newMenuBtn, menuBtn);
|
||||
|
||||
newMenuBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = mobileMenu.classList.contains('open');
|
||||
|
||||
if (isOpen) {
|
||||
mobileMenu.classList.remove('open');
|
||||
menuIcon.style.display = 'block';
|
||||
closeIcon.style.display = 'none';
|
||||
} else {
|
||||
mobileMenu.classList.add('open');
|
||||
menuIcon.style.display = 'none';
|
||||
closeIcon.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
// Close menu when clicking a link
|
||||
const mobileLinks = mobileMenu.querySelectorAll('a');
|
||||
mobileLinks.forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
mobileMenu.classList.remove('open');
|
||||
menuIcon.style.display = 'block';
|
||||
closeIcon.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Close menu when clicking outside
|
||||
const closeHandler = (e) => {
|
||||
if (!mobileMenu.contains(e.target) && !newMenuBtn.contains(e.target)) {
|
||||
if (mobileMenu.classList.contains('open')) {
|
||||
mobileMenu.classList.remove('open');
|
||||
menuIcon.style.display = 'block';
|
||||
closeIcon.style.display = 'none';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Remove old handler if exists
|
||||
document.removeEventListener('click', closeHandler);
|
||||
document.addEventListener('click', closeHandler);
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initMobileMenu);
|
||||
} else {
|
||||
initMobileMenu();
|
||||
}
|
||||
|
||||
// Re-initialize after view transitions
|
||||
document.addEventListener('astro:page-load', initMobileMenu);
|
||||
</script>
|
||||
<nav class="w-full px-4 py-4 bg-transparent">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<a href="/" class="text-xl font-semibold header-text whitespace-nowrap">
|
||||
{metaData.title}
|
||||
</a>
|
||||
|
||||
<ul class="flex flex-wrap items-center gap-2 text-sm text-neutral-700 dark:text-neutral-300">
|
||||
{navItems.map((item, index) => {
|
||||
// if (item.blockSeparator) {
|
||||
// return (
|
||||
// <li class="text-neutral-500 dark:text-neutral-500 px-2 select-none" aria-hidden="true">
|
||||
// ‖
|
||||
// </li>
|
||||
// );
|
||||
// }
|
||||
<nav class="w-full px-4 sm:px-6 py-4 bg-transparent sticky top-0 z-50 backdrop-blur-sm bg-white/80 dark:bg-[#121212]/80 border-b border-neutral-200/50 dark:border-neutral-800/50">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Logo/Brand -->
|
||||
<a
|
||||
href="/"
|
||||
class="text-xl sm:text-2xl font-semibold header-text whitespace-nowrap hover:opacity-80 transition-opacity"
|
||||
>
|
||||
{metaData.title}
|
||||
</a>
|
||||
|
||||
const isExternal = item.href?.startsWith("http");
|
||||
const isAuthedPath = item.auth ?? false;
|
||||
const normalize = (url) => (url || '/').replace(/\/+$/, '') || '/';
|
||||
const normalizedCurrent = normalize(currentPath);
|
||||
const normalizedHref = normalize(item.href);
|
||||
const isActive = !isExternal && (
|
||||
normalizedHref === '/'
|
||||
? normalizedCurrent === '/' // Home only matches exact /
|
||||
: normalizedCurrent === normalizedHref || normalizedCurrent.startsWith(normalizedHref + '/')
|
||||
);
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="desktop-nav flex items-center gap-1">
|
||||
<ul class="flex items-center gap-1">
|
||||
{navItems.map((item) => {
|
||||
const isExternal = item.href?.startsWith("http");
|
||||
const isAuthedPath = item.auth ?? false;
|
||||
const normalize = (url) => (url || '/').replace(/\/+$/, '') || '/';
|
||||
const normalizedCurrent = normalize(currentPath);
|
||||
const normalizedHref = normalize(item.href);
|
||||
const isActive = !isExternal && (
|
||||
normalizedHref === '/'
|
||||
? normalizedCurrent === '/'
|
||||
: normalizedCurrent === normalizedHref || normalizedCurrent.startsWith(normalizedHref + '/')
|
||||
);
|
||||
|
||||
|
||||
const nextItem = navItems[index + 1];
|
||||
const shouldShowThinBar = nextItem //&& !nextItem.blockSeparator;
|
||||
|
||||
return (
|
||||
<>
|
||||
<li>
|
||||
<a
|
||||
href={item.href}
|
||||
class={`flex items-center gap-1 px-2 py-1 rounded-md transition-colors
|
||||
hover:bg-neutral-200 dark:hover:bg-neutral-800
|
||||
${isActive ? "font-semibold underline underline-offset-4" : ""}`}
|
||||
target={isExternal ? "_blank" : undefined}
|
||||
rel={(isExternal || isAuthedPath) ? "external" : undefined}
|
||||
>
|
||||
{item.label}
|
||||
{item.icon === ExitToApp && <ExitToApp className="w-4 h-4" client:load />}
|
||||
</a>
|
||||
</li>
|
||||
{shouldShowThinBar && (
|
||||
<li class="text-neutral-400 dark:text-neutral-600 select-none" aria-hidden="true">
|
||||
|
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={item.href}
|
||||
class={isActive
|
||||
? "flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 bg-neutral-900 dark:bg-neutral-100 text-white dark:text-neutral-900"
|
||||
: "flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800"
|
||||
}
|
||||
target={isExternal ? "_blank" : undefined}
|
||||
rel={(isExternal || isAuthedPath) ? "external" : undefined}
|
||||
>
|
||||
{item.label}
|
||||
{item.icon === ExitToApp && (
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
||||
</svg>
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
<li>
|
||||
<!-- Theme Toggle Desktop -->
|
||||
<button
|
||||
id="theme-toggle"
|
||||
aria-label="Toggle theme"
|
||||
type="button"
|
||||
class="flex items-center justify-center px-2 py-1 rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-opacity"
|
||||
class="flex items-center justify-center w-9 h-9 ml-2 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
|
||||
onclick="toggleTheme()"
|
||||
>
|
||||
<Icon
|
||||
name="fa6-solid:circle-half-stroke"
|
||||
class="h-4 w-4 text-[#1c1c1c] dark:text-[#D4D4D4]"
|
||||
class="h-5 w-5 text-[#1c1c1c] dark:text-[#D4D4D4]"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<div class="mobile-nav flex items-center gap-2">
|
||||
<!-- Theme Toggle Mobile (visible) -->
|
||||
<button
|
||||
aria-label="Toggle theme"
|
||||
type="button"
|
||||
class="flex items-center justify-center w-9 h-9 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
|
||||
onclick="toggleTheme()"
|
||||
>
|
||||
<Icon
|
||||
name="fa6-solid:circle-half-stroke"
|
||||
class="h-5 w-5 text-[#1c1c1c] dark:text-[#D4D4D4]"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="mobile-menu-btn"
|
||||
aria-label="Toggle menu"
|
||||
type="button"
|
||||
class="flex items-center justify-center w-9 h-9 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
|
||||
>
|
||||
<svg id="menu-icon" class="w-6 h-6 text-neutral-700 dark:text-neutral-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
<svg id="close-icon" class="w-6 h-6 text-neutral-700 dark:text-neutral-300" style="display: none;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Navigation Menu -->
|
||||
<div
|
||||
id="mobile-menu"
|
||||
class="mobile-menu-dropdown md:hidden"
|
||||
>
|
||||
<ul class="flex flex-col gap-1 py-4">
|
||||
{navItems.map((item) => {
|
||||
const isExternal = item.href?.startsWith("http");
|
||||
const isAuthedPath = item.auth ?? false;
|
||||
const normalize = (url) => (url || '/').replace(/\/+$/, '') || '/';
|
||||
const normalizedCurrent = normalize(currentPath);
|
||||
const normalizedHref = normalize(item.href);
|
||||
const isActive = !isExternal && (
|
||||
normalizedHref === '/'
|
||||
? normalizedCurrent === '/'
|
||||
: normalizedCurrent === normalizedHref || normalizedCurrent.startsWith(normalizedHref + '/')
|
||||
);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={item.href}
|
||||
class={isActive
|
||||
? "flex items-center justify-between px-4 py-3 rounded-lg text-base font-medium transition-all duration-200 bg-neutral-900 dark:bg-neutral-100 text-white dark:text-neutral-900"
|
||||
: "flex items-center justify-between px-4 py-3 rounded-lg text-base font-medium transition-all duration-200 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800"
|
||||
}
|
||||
target={isExternal ? "_blank" : undefined}
|
||||
rel={(isExternal || isAuthedPath) ? "external" : undefined}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
{item.icon === ExitToApp && (
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
||||
</svg>
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
/* Desktop navigation - visible on medium screens and up */
|
||||
.desktop-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.desktop-nav {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile navigation - visible below medium screens */
|
||||
.mobile-nav {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.mobile-nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile menu dropdown */
|
||||
.mobile-menu-dropdown {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mobile-menu-dropdown.open {
|
||||
max-height: 500px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.mobile-menu-dropdown {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
20
src/middleware.js
Normal file
20
src/middleware.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineMiddleware } from 'astro:middleware';
|
||||
|
||||
export const onRequest = defineMiddleware(async (context, next) => {
|
||||
try {
|
||||
// Let Astro handle the request first
|
||||
const response = await next();
|
||||
|
||||
// If it's a 404, redirect to home
|
||||
if (response.status === 404) {
|
||||
console.log(`404 redirect: ${context.url.pathname} -> /`);
|
||||
return context.redirect('/', 302);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Handle any middleware errors by redirecting to home
|
||||
console.error('Middleware error:', error);
|
||||
return context.redirect('/', 302);
|
||||
}
|
||||
});
|
||||
@@ -1,11 +1,8 @@
|
||||
const getRobotsTxt = (sitemapURL) => `
|
||||
const getRobotsTxt = () => `
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: ${sitemapURL.href}
|
||||
`;
|
||||
|
||||
export const GET = ({ site }) => {
|
||||
const sitemapURL = new URL("sitemap-index.xml", site);
|
||||
return new Response(getRobotsTxt(sitemapURL));
|
||||
export const GET = () => {
|
||||
return new Response(getRobotsTxt());
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user