跳到内容
A Astro Rocket

Markdown test

Markdown test

B

Beyond.ms

2 分钟阅读

Astro Rocket inherits a complete UI component library from Velocity by Southwell Media. That means 57 production-ready components are available the moment you install the theme — no npm packages to install, no extra setup. Every component is already styled with the design system’s color tokens, so they adapt automatically when you switch themes or toggle dark mode.

A second-level heading

A third-level heading

Text that is not a quote

Text that is a quote

Use git status to list all new or modified files that haven’t yet been committed.

Some basic Git commands are:

git status
git add
git commit

The background color is #ffffff for light mode and #000000 for dark mode.

This site was built using GitHub Pages.

Screenshot of a comment on a GitHub issue showing an image, added in the Markdown, of an Octocat smiling and raising a tentacle.

  • George Washington
  • John Adams
  • Thomas Jefferson
  1. James Madison
  2. James Monroe
  3. John Quincy Adams

test

  1. First list item
    • First nested list item
      • Second nested list item

Task

[!NOTE] Useful information that users should know, even when skimming content.

[!TIP] Helpful advice for doing things better or more easily.

[!IMPORTANT] Key information users need to know to achieve their goal.

[!WARNING] Urgent info that needs immediate user attention to avoid problems.

[!CAUTION] Advises about risks or negative outcomes of certain actions.

test



---
import '@/styles/global.css';
//import '@primer/css/index.scss'; 
import 'remark-github-alerts/styles/github-colors-dark-class.css';
import outfitFont from '@fontsource-variable/outfit/files/outfit-latin-wght-normal.woff2?url';
import manropeFont from '@fontsource-variable/manrope/files/manrope-latin-wght-normal.woff2?url';
import SEO from '@/components/seo/SEO.astro';
import JsonLd from '@/components/seo/JsonLd.astro';
import Analytics from '@/components/layout/Analytics.astro';
import ConsentBanner from '@/components/ui/overlay/ConsentBanner';
import CursorTrail from '@/components/effects/CursorTrail.astro';
import { createWebsiteSchema, createOrganizationSchema, createPersonSchema, createProfessionalServiceSchema } from '@/lib/schema';
import type { Thing, WithContext } from 'schema-dts';
import siteConfig from '@/config/site.config';
import { t, getLocaleFromPath } from '@/i18n';

interface Props {
  title?: string;
  description?: string;
  image?: string;
  imageAlt?: string;
  article?: {
    publishedTime?: Date;
    modifiedTime?: Date;
    authors?: string[];
    tags?: string[];
  };
  noindex?: boolean;
  nofollow?: boolean;
  includeOrgSchema?: boolean;
  includePersonSchema?: boolean;
  includeProfessionalServiceSchema?: boolean;
  extraSchemas?: WithContext<Thing>[];
  eagerReveal?: boolean;
  /** Verified locale alternates for this page, forwarded to SEO for hreflang. */
  localeAlternates?: { locale: string; url: string }[];
}

const {
  title,
  description,
  image,
  imageAlt,
  article,
  noindex = false,
  nofollow = false,
  includeOrgSchema = false,
  includePersonSchema = false,
  includeProfessionalServiceSchema = false,
  extraSchemas = [],
  eagerReveal = false,
  localeAlternates,
} = Astro.props;

// Build JSON-LD schemas
const schemas: WithContext<Thing>[] = [createWebsiteSchema()];
if (includeOrgSchema) {
  schemas.push(createOrganizationSchema());
}
if (includePersonSchema) {
  schemas.push(createPersonSchema());
}
if (includeProfessionalServiceSchema) {
  schemas.push(createProfessionalServiceSchema());
}
schemas.push(...extraSchemas);

const locale = getLocaleFromPath(Astro.url.pathname);
---

<!doctype html>
<html lang={locale} class="scroll-smooth dark" data-theme="blue" data-theme-mode="system">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="generator" content={Astro.generator} />

    <!-- Preload display + body fonts so they're ready for first paint -->
    <link rel="preload" as="font" type="font/woff2" href={manropeFont} crossorigin="anonymous" />
    <link rel="preload" as="font" type="font/woff2" href={outfitFont} crossorigin="anonymous" />

    {siteConfig.articleFeatures?.comments?.enabled && (
      <link
        rel="preconnect"
        href={
          siteConfig.articleFeatures?.comments?.provider === 'cusdis'
            ? (siteConfig.articleFeatures?.comments?.cusdis?.host ??
              'https://cusdis.com')
            : 'https://giscus.app'
        }
        crossorigin
      />
    )}

    <!-- Favicon. The SVG letter is outlined to a vector path at build time (see
         src/lib/favicon), and PNG/ICO fallbacks are provided so search-engine
         crawlers, Safari and legacy browsers all render the brand mark. -->
    <link rel="icon" type="image/svg+xml" href={siteConfig.branding.favicon.svg} />
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
    <link rel="icon" href="/favicon.ico" sizes="any" />
    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
    <link rel="manifest" href="/manifest.webmanifest" />
    <meta name="theme-color" content={siteConfig.branding.colors.themeColor} />

    <!-- SEO -->
    <SEO
      title={title}
      description={description}
      image={image}
      imageAlt={imageAlt}
      article={article}
      noindex={noindex}
      nofollow={nofollow}
      localeAlternates={localeAlternates}
    />

    <!-- RSS Feed -->
    <link rel="alternate" type="application/rss+xml" title={`${siteConfig.name} RSS Feed`} href="/rss.xml" />

    <!-- JSON-LD Structured Data -->
    <JsonLd schema={schemas} />

    <!-- Analytics (loads if PUBLIC_GA_MEASUREMENT_ID or PUBLIC_GTM_ID is set) -->
    <Analytics />

    <!-- Client-side view transitions are intentionally disabled. They composed
         poorly with the theme's CSS keyframe animations (hero slide-up,
         data-reveal fades) and produced a visible post-fade "aftershake" on
         mobile. Each navigation now does a normal page load, so animations
         run cleanly from frame 0 every time. -->


    <!-- The favicon is a single static brand mark (see the Favicon block above).
         It intentionally does NOT re-tint to the active theme at runtime: the
         logo must stay identical across every context — browser tab, search
         results, social previews and home-screen icon. -->

    <!-- Theme bootstrap (runs before body paint; no flash of wrong theme).
         Implements a 3-state colour-mode contract:
           localStorage.theme ∈ {'system','light','dark'}  (default 'system')
           <html data-theme-mode="…">                      (mirrors saved mode)
           <html class="dark">                              (resolved appearance)
         When mode === 'system', <html>.dark tracks prefers-color-scheme live. -->
    <script is:inline>
      (function () {
        const COLOR_THEMES = ['orange', 'amber', 'lime', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple', 'magenta'];
        const VALID_MODES = ['system', 'light', 'dark'];

        function getMode() {
          try {
            const stored = localStorage.getItem('theme');
            if (VALID_MODES.indexOf(stored) !== -1) return stored;
          } catch { /* private mode / disabled storage */ }
          return 'system';
        }

        function applyMode(el) {
          const mode = getMode();
          el.setAttribute('data-theme-mode', mode);
          const isDark =
            mode === 'dark' ||
            (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
          if (isDark) el.classList.add('dark');
          else el.classList.remove('dark');
        }

        function applyColorTheme(el) {
          try {
            // Color palette — sessionStorage so it resets to default on each new visit
            const saved = sessionStorage.getItem('color-theme');
            if (saved && COLOR_THEMES.indexOf(saved) !== -1) {
              el.setAttribute('data-theme', saved);
            }
          } catch { /* ignored */ }
        }

        function applyTheme(el) {
          applyMode(el);
          applyColorTheme(el);
        }

        applyTheme(document.documentElement);

        if (!window.__themeListenersInit) {
          window.__themeListenersInit = true;

          // Live-update when the OS flips colour scheme AND the user is on 'system'.
          const mql = window.matchMedia('(prefers-color-scheme: dark)');
          const onSchemeChange = function () {
            if (getMode() === 'system') applyMode(document.documentElement);
          };
          if (mql.addEventListener) mql.addEventListener('change', onSchemeChange);
          else if (mql.addListener) mql.addListener(onSchemeChange); // Safari < 14

          // Re-apply across Astro view transitions (no flash on swap).
          document.addEventListener('astro:before-swap', function (e) {
            applyTheme(e.newDocument.documentElement);
          });
          document.addEventListener('astro:after-swap', function () {
            applyTheme(document.documentElement);
          });
        }
      })();
    </script>

    <style is:global>
      /* Ensure the body container conforms to GitHub's standards */
      .markdown-body {
        box-sizing: border-box;
        min-width: 200px;
        max-width: 980px;
        margin: 0 auto;
        padding: 45px;
      }

      /* Optional: Standard responsive mobile padding tweak */
      @media (max-width: 767px) {
        .markdown-body {
          padding: 15px;
        }
      }
    </style>
  </head>

  <body class="min-h-screen bg-background text-foreground antialiased" data-eager-reveal={eagerReveal ? '' : undefined}>
    <!-- Skip to content link -->
    <a
      href="#main-content"
      class="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-50 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-primary-foreground"
    >
      {t('nav.skipToContent', locale)}
    </a>

    <slot name="header" />

    <main id="main-content" class="flex-1">
      <slot />
    </main>

    <slot name="footer" />

    <ConsentBanner />

    {siteConfig.effects?.cursorTrail !== false && <CursorTrail />}

    <!-- Back to top button -->
    <button
      id="back-to-top"
      aria-label={t('common.backToTop', locale)}
      class="fixed bottom-6 right-6 z-50 h-12 w-12"
      style="opacity:0;translate:0 2rem;pointer-events:none"
    >
      <!-- Scroll-progress ring (rotated so arc starts at 12 o'clock) -->
      <svg class="absolute inset-0 h-full w-full -rotate-90" viewBox="0 0 48 48" aria-hidden="true">
        <!-- Faint grey track -->
        <circle cx="24" cy="24" r="21" fill="none" stroke="currentColor" class="text-border" stroke-width="2" opacity="0.5" />
        <!-- Brand progress arc -->
        <circle id="back-to-top-ring" cx="24" cy="24" r="21" fill="none" stroke="currentColor" class="text-brand-500" stroke-width="2.5" stroke-linecap="round" stroke-dasharray="131.95" stroke-dashoffset="131.95" />
      </svg>
      <!-- Inner face -->
      <span class="absolute inset-[5px] flex items-center justify-center rounded-full bg-background border border-border-strong text-foreground-muted shadow-md" aria-hidden="true">
        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m18 15-6-6-6 6"/></svg>
      </span>
    </button>

    <script is:inline>
      (function () {
        const THRESHOLD = 400;
        const CIRCUMFERENCE = 131.95;

        // Cache `scrollHeight - innerHeight` so the per-frame scroll handler
        // never reads layout-dependent properties. Reading scrollHeight on
        // DOMContentLoaded forced a synchronous layout BEFORE first paint
        // (Lighthouse flagged this script as ~551ms of forced reflow). The
        // first read is now deferred to a rAF inside initBackToTop — that
        // runs after the browser has committed initial paint, off the
        // critical path. ResizeObserver picks up later changes.
        let docMaxScrollY = 0;
        function updateDocMaxScrollY() {
          docMaxScrollY = document.documentElement.scrollHeight - window.innerHeight;
        }
        window.addEventListener('resize', updateDocMaxScrollY, { passive: true });
        if (typeof ResizeObserver !== 'undefined') {
          new ResizeObserver(updateDocMaxScrollY).observe(document.documentElement);
        }

        function initBackToTop() {
          const btn = document.getElementById('back-to-top');
          if (!btn || btn.dataset.bttInit) return;
          btn.dataset.bttInit = 'true';

          const ring = document.getElementById('back-to-top-ring');
          const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
          let ticking = false;
          let visible = false;

          // Ensure initial hidden state is applied via JS (guards against any cascade override)
          btn.style.opacity = '0';
          btn.style.translate = '0 2rem';
          btn.style.pointerEvents = 'none';

          function show() {
            if (reducedMotion) {
              btn.style.transition = 'none';
            } else {
              btn.style.transition = 'opacity 250ms cubic-bezier(0,0,0.2,1), translate 250ms cubic-bezier(0,0,0.2,1)';
            }
            btn.style.opacity = '1';
            btn.style.translate = '0 0';
            btn.style.pointerEvents = 'auto';
            visible = true;
          }

          function hide() {
            if (reducedMotion) {
              btn.style.transition = 'none';
            } else {
              btn.style.transition = 'opacity 300ms cubic-bezier(0.4,0,1,1), translate 300ms cubic-bezier(0.4,0,1,1)';
            }
            btn.style.opacity = '0';
            btn.style.translate = '0 2rem';
            btn.style.pointerEvents = 'none';
            visible = false;
          }

          function updateFrame() {
            const pct = docMaxScrollY > 0 ? Math.min(1, Math.max(0, window.scrollY / docMaxScrollY)) : 0;
            if (ring) {
              ring.style.strokeDashoffset = String(CIRCUMFERENCE * (1 - pct));
            }
            if (window.scrollY > THRESHOLD) {
              if (!visible) show();
            } else {
              if (visible) hide();
            }
            ticking = false;
          }

          function onScroll() {
            if (!ticking) {
              ticking = true;
              requestAnimationFrame(updateFrame);
            }
          }

          window.addEventListener('scroll', onScroll, { passive: true });
          btn.addEventListener('click', function () {
            window.scrollTo({ top: 0, behavior: reducedMotion ? 'auto' : 'smooth' });
          });

          document.addEventListener('astro:before-swap', function () {
            window.removeEventListener('scroll', onScroll);
          }, { once: true });

          // Run the first frame on rAF so it picks up the initial scroll
          // position after layout has settled — and read scrollHeight here
          // (after first paint) instead of forcing layout pre-paint.
          requestAnimationFrame(function () {
            updateDocMaxScrollY();
            updateFrame();
          });
        }

        document.addEventListener('astro:page-load', initBackToTop);
        initBackToTop();
      })();
    </script>

    <script is:inline>
      (function () {
        function initReveal() {
          const els = Array.prototype.slice.call(
            document.querySelectorAll('[data-reveal]:not(.is-visible), [data-reveal-children]:not(.is-visible)')
          );

          // Also observe each direct child of [data-reveal-content] so long
          // article bodies (blog posts, project pages) animate block-by-block.
          document.querySelectorAll('[data-reveal-content]').forEach(function (container) {
            Array.prototype.forEach.call(container.children, function (child) {
              if (!child.classList.contains('is-visible')) els.push(child);
            });
          });

          if (!els.length) return;

          // Non-eager pages trigger a bit deeper in the viewport (-15% bottom)
          // so a block is comfortably on-screen before it rises — its slide-up
          // plays in front of the reader instead of being masked by the scroll
          // as it peeks in at the bottom edge. Eager pages keep the earlier -5%
          // so their first above-fold cards still reveal promptly.
          const eager = document.body.hasAttribute('data-eager-reveal');
          const belowFoldMargin = eager ? '0px 0px -5% 0px' : '0px 0px -15% 0px';

          // Two scroll-triggered observers for below-fold elements. Tall
          // elements (taller than ~85% of the viewport — long code blocks,
          // large embeds) use threshold 0 so they still fire; shorter
          // elements use 0.15 so animations feel earned.
          const shortObs = new IntersectionObserver(function (entries) {
            entries.forEach(function (entry) {
              if (entry.isIntersecting) {
                entry.target.classList.add('is-visible');
                shortObs.unobserve(entry.target);
              }
            });
          }, { threshold: 0.15, rootMargin: belowFoldMargin });

          const tallObs = new IntersectionObserver(function (entries) {
            entries.forEach(function (entry) {
              if (entry.isIntersecting) {
                entry.target.classList.add('is-visible');
                tallObs.unobserve(entry.target);
              }
            });
          }, { threshold: 0, rootMargin: belowFoldMargin });

          // First-pass observer: hands us boundingClientRect and rootBounds
          // for each element without forcing a synchronous layout read. The
          // browser computes the geometry off the main thread, so this
          // replaces the previous getBoundingClientRect() loop that caused
          // Lighthouse to flag a 76ms forced reflow.
          //
          // rootMargin -25% on the bottom defines "above-the-fold" as the
          // top 75% of the viewport, matching the prior `rect.top < vh*0.75`
          // condition.
          let revealIndex = 0;
          const firstPass = new IntersectionObserver(function (entries) {
            // Preserve document order so the above-fold stagger is stable.
            entries.sort(function (a, b) {
              const pos = a.target.compareDocumentPosition(b.target);
              if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
              if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1;
              return 0;
            });

            entries.forEach(function (entry) {
              firstPass.unobserve(entry.target);
              if (entry.isIntersecting) {
                const i = revealIndex++;
                setTimeout(function () {
                  entry.target.classList.add('is-visible');
                }, 250 + i * 80);
              } else {
                const rb = entry.rootBounds;
                const tall = rb ? entry.boundingClientRect.height > rb.height * 0.85 : false;
                (tall ? tallObs : shortObs).observe(entry.target);
              }
            });
          }, { threshold: 0, rootMargin: '0px 0px -25% 0px' });

          // Cards flagged data-reveal-eager (the first card on a listing page)
          // reveal on load with the normal animation instead of waiting for
          // scroll. On mobile the centered hero can push the first card below
          // firstPass's fold line, which would otherwise leave it hidden until
          // the reader scrolls — unlike pages with no heading above the grid
          // (e.g. projects), where the first card sits above the fold and
          // animates on load. The 250ms delay mirrors firstPass's base reveal
          // timing so the entrance matches, and lets the opacity:0 frame paint
          // first so the transition plays.
          //
          // On a multi-column grid the eager card's same-row neighbours would
          // otherwise ride the scroll observers on a different clock, so a
          // side-by-side pair enters visibly out of sync. Group each eager card
          // with its same-row [data-reveal] siblings and reveal them in one
          // tick. "Same row" is detected by a shared offsetTop, so on a stacked
          // single-column layout (mobile) nothing qualifies and the existing
          // top-to-bottom stagger is left untouched. The offsetTop reads happen
          // in a single pass with no interleaved writes, so they share one
          // layout flush rather than thrashing.
          const eagerGroups = new Map();
          const syncedSiblings = new Set();
          els.forEach(function (el) {
            if (!el.hasAttribute('data-reveal-eager')) return;
            const group = [el];
            const parent = el.parentElement;
            if (parent) {
              const top = el.offsetTop;
              Array.prototype.forEach.call(parent.children, function (sib) {
                if (
                  sib !== el &&
                  sib.hasAttribute('data-reveal') &&
                  !sib.hasAttribute('data-reveal-eager') &&
                  Math.abs(sib.offsetTop - top) <= 1
                ) {
                  group.push(sib);
                  syncedSiblings.add(sib);
                }
              });
            }
            eagerGroups.set(el, group);
          });

          els.forEach(function (el) {
            if (el.hasAttribute('data-reveal-eager')) {
              const group = eagerGroups.get(el);
              setTimeout(function () {
                group.forEach(function (node) { node.classList.add('is-visible'); });
              }, 250);
              return;
            }
            // Already revealed in lockstep with its eager row-mate above.
            if (syncedSiblings.has(el)) return;
            firstPass.observe(el);
          });
        }

        if (document.readyState === 'loading') {
          document.addEventListener('DOMContentLoaded', initReveal);
        } else {
          initReveal();
        }
      })();
    </script>
  </body>
</html>
分享:

blog.cta.heading

blog.cta.description