Files
blog/src/pages/notes/[...slug].astro
T
Hadi 73b668b204 Change link-cards height
Signed-off-by: Hadi <hadi@example.com>
2026-05-04 15:19:20 +02:00

341 lines
13 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import { getCollection, render } from "astro:content";
import Layout from "../../layouts/Layout.astro";
import { List, PanelRight } from "@lucide/astro";
import NoteTOC from "../../components/NoteTOC.astro";
import NoteNavSidebar from "../../components/NoteNavSidebar.svelte";
import NoteGraphSidebar from "../../components/NoteGraphSidebar.astro";
import NoteVars from "../../components/NoteVars.svelte";
import { getCategory, extractLinks, extractHeadings } from "../../utils/notes";
export async function getStaticPaths() {
const notes = await getCollection("notes");
return notes.map((entry) => ({
params: { slug: entry.id },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await render(entry);
const allNotes = await getCollection("notes");
const sortedNotes = allNotes.sort((a, b) =>
a.data.title.localeCompare(b.data.title),
);
const categories = [...new Set(allNotes.map(getCategory))].sort();
const allLinks = Object.fromEntries(
allNotes.map((n) => [n.id, extractLinks(n.body ?? "")]),
);
const forwardLinks = (allLinks[entry.id] ?? [])
.map((id) => allNotes.find((n) => n.id === id))
.filter(Boolean) as typeof allNotes;
const backlinks = allNotes.filter(
(n) => n.id !== entry.id && (allLinks[n.id] ?? []).includes(entry.id),
);
const graphNodes = [
{ id: entry.id, title: entry.data.title, current: true },
...forwardLinks.map((n) => ({
id: n.id,
title: n.data.title,
current: false,
})),
...backlinks
.filter((n) => !forwardLinks.some((f) => f.id === n.id))
.map((n) => ({ id: n.id, title: n.data.title, current: false })),
];
const graphEdges = [
...forwardLinks.map((n) => ({ from: entry.id, to: n.id })),
...backlinks.map((n) => ({ from: n.id, to: entry.id })),
];
const noteVars = [
...new Set(
Array.from(
(entry.body ?? "").matchAll(/\$([a-zA-Z_][a-zA-Z0-9_]*)/g),
(m) => m[1],
),
),
];
const headings = extractHeadings(entry.body ?? "");
---
<style>
.drawer.lg\:drawer-open > .drawer-side,
.drawer.xl\:drawer-open > .drawer-side {
top: 3rem;
height: calc(100vh - 3rem);
}
</style>
<Layout
title={`${entry.data.title} - Infosec Notes`}
description={entry.data.description}
>
<main class="max-w-screen-2xl mx-auto">
<div class="drawer drawer-end xl:drawer-open min-h-[calc(100vh-3rem)]">
<input id="graph-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex min-h-[calc(100vh-3rem)]">
<div class="drawer lg:drawer-open w-full">
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col min-w-0">
<main class="flex-1 px-4 sm:px-6 lg:px-10 py-6 lg:py-10 min-w-0">
<div class="max-w-3xl mx-auto lg:mx-0">
<div class="flex items-center justify-between mb-6">
<div
class="breadcrumbs text-xs font-mono text-base-content/35 p-0"
>
<ul>
<li>
<a href="/notes" class="hover:text-base-content/70"
>notes</a
>
</li>
<li>
{
entry.id.includes("/") ? (
<a
href={`/notes/${getCategory(entry)}`}
class="hover:text-base-content/70"
>
{getCategory(entry)}
</a>
) : (
getCategory(entry)
)
}
</li>
</ul>
</div>
<div class="flex items-center gap-2">
<label
for="nav-drawer"
class="btn btn-ghost btn-xs lg:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
>
<List size={11} />
nav
</label>
<NoteVars client:load vars={noteVars} />
<label
for="graph-drawer"
id="graph-toggle"
class="btn btn-ghost btn-xs font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
title="Toggle graph"
>
<PanelRight size={11} />
graph
</label>
</div>
</div>
<header class="mb-8">
<h1
class="text-4xl sm:text-5xl font-bold tracking-tight mb-3"
>
{entry.data.title}
</h1>
<p class="text-base-content/50 mb-4">
{entry.data.description}
</p>
{
entry.data.tags.length > 0 && (
<div class="flex flex-wrap gap-1 mb-4">
{entry.data.tags.map((tag) => (
<a
href={`/notes?tag=${tag}`}
class="badge badge-ghost badge-xs font-mono text-base-content/30 hover:text-primary/70 transition-colors"
>
{tag}
</a>
))}
</div>
)
}
</header>
<NoteTOC headings={headings} />
<div
class="note-content text-sm leading-relaxed text-base-content/80
[&_h2]:text-lg [&_h2]:font-bold [&_h2]:mt-8 [&_h2]:mb-3 [&_h2]:text-base-content [&_h2]:tracking-tight [&_h2]:pb-1.5 [&_h2]:border-b [&_h2]:border-base-300/30
[&_h3]:text-base [&_h3]:font-semibold [&_h3]:mt-6 [&_h3]:mb-2 [&_h3]:text-base-content/90
[&_h4]:text-sm [&_h4]:font-semibold [&_h4]:mt-4 [&_h4]:mb-2 [&_h4]:text-base-content/80
[&_p]:mb-4 [&_p]:leading-relaxed
[&_a]:text-primary/80 [&_a]:underline [&_a]:underline-offset-2 [&_a]:hover:text-primary [&_a]:transition-colors
[&_ul]:mb-4 [&_ul]:ml-5 [&_ul]:list-none [&_ul]:space-y-1
[&_ul_li]:before:content-[''] [&_ul_li]:before:text-base-content/25 [&_ul_li]:before:mr-2 [&_ul_li]:before:font-mono
[&_ol]:mb-4 [&_ol]:ml-5 [&_ol]:list-decimal [&_ol]:space-y-1
[&_li]:text-base-content/75
[&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded-field [&_code]:font-mono [&_code]:text-xs [&_code]:bg-base-200 [&_code]:text-primary/80 [&_code]:border [&_code]:border-base-300/50
[&_pre]:p-4 [&_pre]:overflow-x-auto [&_pre]:mb-4 [&_pre]:rounded-box [&_pre]:bg-base-200/60 [&_pre]:border [&_pre]:border-base-300/50 [&_pre]:text-xs
[&_pre_code]:bg-transparent [&_pre_code]:border-0 [&_pre_code]:p-0 [&_pre_code]:text-base-content/80
[&_blockquote]:border-l-2 [&_blockquote]:border-primary/25 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:my-4 [&_blockquote]:text-base-content/50
[&_table]:w-full [&_table]:mb-6 [&_table]:text-xs [&_table]:border-collapse
[&_th]:text-left [&_th]:px-3 [&_th]:py-2 [&_th]:border [&_th]:border-base-300/50 [&_th]:bg-base-200/60 [&_th]:font-mono [&_th]:text-[10px] [&_th]:uppercase [&_th]:tracking-widest [&_th]:text-base-content/50
[&_td]:px-3 [&_td]:py-2 [&_td]:border [&_td]:border-base-300/40 [&_td]:font-mono [&_td]:text-xs [&_td]:text-base-content/70
[&_tr:nth-child(even)_td]:bg-base-200/20
[&_hr]:border-t [&_hr]:border-base-300/30 [&_hr]:my-8"
>
<Content />
</div>
<div
class="border-t border-base-300/30 mt-12 pt-6 flex items-center justify-between font-mono text-[10px] text-base-content/25"
>
<a
href="/notes"
class="hover:text-base-content/50 transition-colors"
>
← all notes
</a>
<a
href="/"
class="hover:text-base-content/50 transition-colors"
>
~/hadi
</a>
</div>
</div>
</main>
</div>
<div class="drawer-side z-50">
<label
for="nav-drawer"
aria-label="close sidebar"
class="drawer-overlay"></label>
<NoteNavSidebar
client:load
notes={sortedNotes}
currentEntry={entry}
categories={categories}
/>
</div>
</div>
</div>
<div class="drawer-side z-40">
<label
for="graph-drawer"
aria-label="close sidebar"
class="drawer-overlay xl:hidden"></label>
<NoteGraphSidebar
entry={entry}
graphNodes={graphNodes}
graphEdges={graphEdges}
forwardLinks={forwardLinks}
backlinks={backlinks}
/>
</div>
</div>
</main>
</Layout>
<script>
function injectHeadingAnchors() {
if (!document.getElementById("heading-anchor-styles")) {
const s = document.createElement("style");
s.id = "heading-anchor-styles";
s.textContent = `
.note-content h2:not(.link-card h2),
.note-content h3:not(.link-card h3),
.note-content h4:not(.link-card h4) {
display: flex !important;
align-items: center;
flex-wrap: wrap;
gap: 0;
}
.heading-anchor {
display: inline-flex;
align-items: center;
flex-shrink: 0;
margin-left: 0.4em;
color: oklch(38% 0 0);
opacity: 0;
transition: opacity 120ms, color 120ms;
text-decoration: none;
}
.note-content h2:hover .heading-anchor,
.note-content h3:hover .heading-anchor,
.note-content h4:hover .heading-anchor { opacity: 1; }
.heading-anchor:hover, .heading-anchor.copied { color: oklch(71% 0.0863 296.59); opacity: 1; }
`;
document.head.appendChild(s);
}
document
.querySelectorAll(".note-content h2, .note-content h3, .note-content h4")
.forEach((heading) => {
if (!heading.id || heading.querySelector(".heading-anchor")) return;
const anchor = document.createElement("a");
anchor.href = `#${heading.id}`;
anchor.className = "heading-anchor";
anchor.setAttribute("aria-label", "Copy link to section");
anchor.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`;
anchor.addEventListener("click", (e) => {
e.preventDefault();
const url = `${location.origin}${location.pathname}#${heading.id}`;
navigator.clipboard.writeText(url).then(() => {
anchor.classList.add("copied");
setTimeout(() => anchor.classList.remove("copied"), 1800);
});
history.pushState(null, "", `#${heading.id}`);
});
heading.appendChild(anchor);
});
}
function initXlGraphToggle() {
const graphDrawer = document.getElementById(
"graph-drawer",
) as HTMLInputElement | null;
if (!graphDrawer) return;
const outerDrawer = graphDrawer.closest<HTMLElement>(".drawer.drawer-end");
const xlQuery = window.matchMedia("(min-width: 1280px)");
const STORAGE_KEY = "notes-graph-sidebar";
function setXlSidebar(open: boolean) {
if (!outerDrawer) return;
if (open) {
outerDrawer.classList.add("xl:drawer-open");
} else {
outerDrawer.classList.remove("xl:drawer-open");
}
localStorage.setItem(STORAGE_KEY, open ? "1" : "0");
}
const graphToggle = document.getElementById("graph-toggle");
graphToggle?.addEventListener("click", (e) => {
if (!xlQuery.matches) return;
e.preventDefault();
setXlSidebar(!outerDrawer?.classList.contains("xl:drawer-open"));
});
if (xlQuery.matches) {
const saved = localStorage.getItem(STORAGE_KEY);
// Open by default unless user explicitly closed it
setXlSidebar(saved !== "0");
}
xlQuery.addEventListener("change", (e) => {
if (!e.matches) {
outerDrawer?.classList.remove("xl:drawer-open");
} else {
const saved = localStorage.getItem(STORAGE_KEY);
setXlSidebar(saved !== "0");
}
});
}
document.addEventListener("astro:page-load", () => {
injectHeadingAnchors();
initXlGraphToggle();
});
</script>