daemon-sec-lotl

DÆMONBins: GTFOBins × LOLBAS × WADComs × LOOBins in one filterable catalog
git clone https://git.daemon-sec.xyz/daemon-sec-lotl.git
Log | Files | Refs | Submodules | README | LICENSE

Base.astro (9618B)


      1 ---
      2 import { ClientRouter } from 'astro:transitions';
      3 import Header from '../components/Header.astro';
      4 import Footer from '../components/Footer.astro';
      5 import SearchModal from '../components/SearchModal.astro';
      6 import { url } from '../lib/url';
      7 import { serialize, type JsonLd } from '../lib/jsonld';
      8 import '../styles/app.css';
      9 // Preload the two faces that paint above the fold — the wordmark in the
     10 // masthead and the DM Mono the masthead/labels use — so the header does not flash
     11 // its fallback on first paint. The rest load on demand via @font-face.
     12 import notoWordmark from '../fonts/noto-sans-display-var-latin.woff2';
     13 import dmMono from '../fonts/dm-mono-400.woff2';
     14 
     15 interface Props {
     16   title?: string;
     17   description?: string;
     18   railless?: boolean;
     19   /** schema.org objects for this page; see src/lib/jsonld.ts */
     20   jsonLd?: JsonLd | JsonLd[];
     21 }
     22 const {
     23   jsonLd,
     24   title = 'DÆMONBins — the Off-the-Land Almanac',
     25   description = 'One filterable catalog merging GTFOBins, LOLBAS, WADComs and LOOBins — plus DÆMON additions. Living-off-the-land and offensive techniques by platform, access, environment and evidence state.',
     26 } = Astro.props;
     27 // Canonical = site origin + slash-less path (`trailingSlash: 'never'` in
     28 // astro.config.mjs; vercel.json 308s `/x/` → `/x`, so a slashed canonical
     29 // would point every page at a redirect). Only `/` keeps its slash.
     30 const canonical = new URL(Astro.url.pathname.replace(/(.)\/$/, '$1'), Astro.site).href;
     31 // Social card: crawlers need an absolute URL and a raster — the PNG is rendered
     32 // from the SVG template by `npm run og` (scripts/og.mjs) and committed.
     33 const ogImage = new URL('og.png', Astro.site).href;
     34 ---
     35 <!doctype html>
     36 <html lang="en" data-theme="light">
     37   <head>
     38     <meta charset="utf-8" />
     39     <meta name="viewport" content="width=device-width, initial-scale=1" />
     40     <title>{title}</title>
     41     <meta name="description" content={description} />
     42     <link rel="canonical" href={canonical} />
     43     <link rel="icon" href={url('favicon.svg')} type="image/svg+xml" />
     44     <link rel="preload" href={notoWordmark} as="font" type="font/woff2" crossorigin />
     45     <link rel="preload" href={dmMono} as="font" type="font/woff2" crossorigin />
     46     <meta name="theme-color" content="#191724" media="(prefers-color-scheme: dark)" />
     47     <meta name="theme-color" content="#faf4ed" media="(prefers-color-scheme: light)" />
     48     <meta property="og:type" content="website" />
     49     <meta property="og:site_name" content="DÆMONBins" />
     50     <meta property="og:url" content={canonical} />
     51     <meta property="og:title" content={title} />
     52     <meta property="og:description" content={description} />
     53     <meta property="og:image" content={ogImage} />
     54     <meta property="og:image:width" content="1200" />
     55     <meta property="og:image:height" content="630" />
     56     <meta property="og:image:alt" content="DÆMONBins — the Off-the-Land Almanac: GTFOBins × LOLBAS × WADComs × LOOBins merged into one catalog" />
     57     <meta name="twitter:card" content="summary_large_image" />
     58     <meta name="twitter:title" content={title} />
     59     <meta name="twitter:description" content={description} />
     60     <meta name="twitter:image" content={ogImage} />
     61     {jsonLd && <script is:inline type="application/ld+json" set:html={serialize(jsonLd)} />}
     62     <!-- Set theme before first paint to avoid FOUC -->
     63     <script is:inline>
     64       (function () {
     65         function apply() {
     66           try {
     67             var display = JSON.parse(localStorage.getItem('daemonbins:display') || '{}');
     68             document.documentElement.dataset.motion = display.motion ? 'reduce' : 'system';
     69             var t = localStorage.getItem('theme');
     70             // No stored choice: follow the OS. Dawn (cream) is the default when
     71             // the OS has no preference, matching the main DÆMON site — but a
     72             // dark-mode reader should not get a cream page under dark chrome.
     73             if (t !== 'light' && t !== 'dark') {
     74               t = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
     75             }
     76             document.documentElement.setAttribute('data-theme', t);
     77             document.documentElement.style.colorScheme = t;
     78           } catch (e) {
     79             document.documentElement.setAttribute('data-theme', 'light');
     80           }
     81         }
     82 
     83         apply();
     84 
     85         /* Every page ships `<html data-theme="light">` in its static markup,
     86            because that is the default and the build has no way to know what
     87            any given reader chose. On a view-transition navigation Astro
     88            swaps in the new document — attributes on <html> included — so
     89            that baked-in "light" overwrites the live attribute and the page
     90            reverts to dawn mid-session.
     91 
     92            This listener re-applies the stored choice on every swap. It runs
     93            in `astro:after-swap`, which fires after the new document is in
     94            place but before it is painted, so the correction never flashes.
     95 
     96            The listener is registered once, from an inline head script that
     97            executes on the first full load only — which is exactly why it
     98            has to be `is:inline` and live here rather than in app.ts: a
     99            bundled module re-imported per page would re-register it, and a
    100            deferred one would run too late to beat the paint. */
    101         document.addEventListener('astro:after-swap', apply);
    102       })();
    103     </script>
    104     <!-- Page iris. Every internal navigation opens the next page as a
    105          circle from the link that was pressed: the theme wipe's gesture,
    106          at page pace. The CSS is beside the theme wipe in global.css;
    107          this only supplies the origin and the end radius. -->
    108     <script is:inline>
    109       (function () {
    110         function centre(el) {
    111           var r = el.getBoundingClientRect();
    112           return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
    113         }
    114 
    115         /* Where the last click landed. Capture phase, so it is recorded
    116            before the ClientRouter's own listener takes the click. A click
    117            with detail 0 came from the keyboard and has no pointer
    118            position, so it opens from the middle of the control. */
    119         var press = null;
    120         document.addEventListener('click', function (e) {
    121           var at = e.detail === 0 && e.target instanceof Element
    122             ? centre(e.target)
    123             : { x: e.clientX, y: e.clientY };
    124           at.t = Date.now();
    125           press = at;
    126         }, true);
    127 
    128         var origin = null;
    129         function paint() {
    130           if (!origin) return;
    131           var s = document.documentElement.style;
    132           s.setProperty('--vt-x', origin.x + 'px');
    133           s.setProperty('--vt-y', origin.y + 'px');
    134           s.setProperty('--vt-r', origin.r + 'px');
    135         }
    136 
    137         /* A press older than a second did not cause this navigation (back,
    138            forward, a scripted one), so it falls back to the link Astro
    139            reports, then to the middle of the screen. The radius reaches
    140            the farthest corner exactly, so the circle finishes as it
    141            covers the page rather than part-way through an overshoot. */
    142         document.addEventListener('astro:before-preparation', function (e) {
    143           var at = press && Date.now() - press.t < 1000 ? press
    144             : e.sourceElement instanceof Element ? centre(e.sourceElement)
    145             : { x: innerWidth / 2, y: innerHeight / 2 };
    146           origin = {
    147             x: at.x,
    148             y: at.y,
    149             r: Math.hypot(Math.max(at.x, innerWidth - at.x), Math.max(at.y, innerHeight - at.y)),
    150           };
    151           paint();
    152         });
    153 
    154         /* The swap replaces <html>'s attributes with the new page's, and
    155            the inline style carrying these three goes with them, so they
    156            are put back before the new page is painted, for the same
    157            reason as the theme note above. */
    158         document.addEventListener('astro:after-swap', paint);
    159       })();
    160     </script>
    161     {import.meta.env.PROD && (
    162       <Fragment>
    163         {/* Vercel Web Analytics + Speed Insights. Zero-dependency: Vercel serves
    164             these scripts at the edge once the features are enabled for the
    165             project (Vercel dashboard → Analytics / Speed Insights). They live in
    166             <head> so Astro's ClientRouter keeps them across view transitions
    167             rather than re-running them per navigation. Privacy-friendly and
    168             cookieless; no-op in dev. */}
    169         <script is:inline>window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };</script>
    170         <script is:inline defer src="/_vercel/insights/script.js"></script>
    171         <script is:inline>window.si = window.si || function () { (window.siq = window.siq || []).push(arguments); };</script>
    172         <script is:inline defer src="/_vercel/speed-insights/script.js"></script>
    173       </Fragment>
    174     )}
    175     <ClientRouter />
    176   </head>
    177   <body>
    178     <a class="skip-link" href="#main">Skip to content</a>
    179     <!-- The film over the whole page. It is what keeps a flat colour
    180          reading as stock rather than as a swatch, and it stands in for the
    181          texture the old glass build got from blur. -->
    182     <span class="grain" aria-hidden="true"></span>
    183     <!-- The dot field behind the top of the page (src/scripts/dmn). -->
    184     <div class="dmn-page-field" aria-hidden="true"><canvas data-dots></canvas></div>
    185     <Header />
    186     <main id="main">
    187       <slot />
    188     </main>
    189     <Footer />
    190     <SearchModal />
    191     <script>
    192       import '../scripts/app.ts';
    193     </script>
    194   </body>
    195 </html>