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.
- George Washington
- John Adams
- Thomas Jefferson
- James Madison
- James Monroe
- John Quincy Adams
test
- First list item
- First nested list item
- Second nested list item
- First nested list item
Task
- #739
- https://github.com/octo-org/octo-repo/issues/740
- Add delight to the experience when all tasks are complete :tada:
- (Optional) Open a followup issue
[!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
Use keyboard shortcuts to navigate faster. Press ⌘K to open the command palette.
Your changes are now live at your-app.vercel.app
You’ve used 80% of your monthly API quota. Consider upgrading your plan.
Error in src/components/Button.tsx — missing required prop “variant”
---
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>