All components from @guides/ui. Source code is read directly from the
package at build time β this page auto-updates whenever the components change.
ποΈ
SiteLayout@guides/ui/SiteLayout.astro
Top-level page wrapper. Composes BaseLayout, SiteNav, and Footer around your slot content.
Pass a SiteConfig object for all branding β theme colour, emoji, nav CTA, footer links, and more.
---
import BaseLayout from './BaseLayout.astro';
import SiteNav from './SiteNav.astro';
import Footer from './Footer.astro';
import ContactPage from './ContactPage.astro';
import ThankYouPage from './ThankYouPage.astro';
export interface SiteConfig {
themeColor: string;
ogSiteName: string;
ogImage?: string;
ogImageAlt?: string;
favicon?: string;
emoji: string;
brandName: string;
navCtaLabel?: string;
navCtaHref?: string;
footerTagline: string;
footerResources: Array<{ label: string; href: string }>;
establishedYear?: number;
copyright?: string;
gaCode?: string;
/** Whether Cloudflare Web Analytics is enabled for this site (via the Pages project's automatic toggle) β surfaced on the privacy page. */
cloudflareAnalytics?: boolean;
/** Show the "A Little Splash Studio product" footer line and studio meta author. Games only; omit for apps, tools and fan projects. */
studio?: boolean;
/** Content for the site's `/privacy` page (rendered by PrivacyPage.astro). */
privacy?: {
lastUpdated?: string;
localStorageDescription?: string;
thirdPartyLinks?: string[];
};
}
interface Props {
config: SiteConfig;
title: string;
description: string;
canonical?: string;
keywords?: string;
heroBg?: string;
noindex?: boolean;
variant?: 'default' | 'contact' | 'thanks';
}
const { config, title, description, canonical, keywords, heroBg, noindex, variant = 'default' } = Astro.props;
---
<BaseLayout
{title}
{description}
{canonical}
{keywords}
{heroBg}
{noindex}
themeColor={config.themeColor}
ogSiteName={config.ogSiteName}
ogImage={config.ogImage}
ogImageAlt={config.ogImageAlt}
favicon={config.favicon}
gaCode={config.gaCode}
author={config.studio ? 'Little Splash Studio' : undefined}
>
<slot name="head" slot="head" />
<SiteNav emoji={config.emoji} brandName={config.brandName} ctaLabel={config.navCtaLabel} appHref={config.navCtaHref} />
{variant === 'contact' && <ContactPage webPageName={config.ogSiteName} />}
{variant === 'thanks' && <ThankYouPage />}
{variant === 'default' && <slot />}
<Footer
logoIcon={config.emoji}
brandName={config.ogSiteName}
tagline={config.footerTagline}
resources={config.footerResources}
establishedYear={config.establishedYear}
copyright={config.copyright}
studio={config.studio}
/>
</BaseLayout>
π
BaseLayout@guides/ui/BaseLayout.astro
Raw HTML shell with full SEO metadata, Open Graph tags, favicon, and optional Google Analytics.
Use this directly when you need complete control over the chrome, or let SiteLayout wrap it for you.
Sticky top navigation bar. Shows a brand logo (emoji + name) linking to /,
a row of nav links (About, FAQ, Contact), and an optional CTA button.
Collapses the nav links on narrow screens.
Preview
Usage
---
import SiteNav from '@guides/ui/SiteNav.astro';
---
<SiteNav
emoji="β"
brandName="My Site"
ctaLabel="Get the app"
appHref="/app/"
/>
Site footer with logo, tagline, resource links, and auto-generated copyright.
External resource links open in a new tab. Renders a gradient top border using
--primary and --accent CSS variables.
Full-width hero banner with a gradient background driven by --primary and its variants.
Accepts a title, subtitle, description, and an optional slot for CTA buttons or badges.
Preview
Dragon Quest Builders 2
The Complete Building Guide
Everything you need to master crafting, building, and exploring.
Usage
---
import HeroSection from '@guides/ui/HeroSection.astro';
---
<HeroSection
title="Dragon Quest Builders 2"
subtitle="The Complete Building Guide"
description="Everything you need to master crafting, building, and exploring."
>
<!-- Optional slot: CTA buttons, badges, etc. -->
<a href="/download" class="cta-button">Download App</a>
</HeroSection>
App download CTA section. Renders a Google Play badge if playStoreUrl is provided,
a plain download button if downloadUrl is given, and/or a "use in browser" web app link.
---
import DownloadSection from '@guides/ui/DownloadSection.astro';
---
<!-- APK from GitHub Releases -->
<DownloadSection
heading="Get the Android App"
description="Install the APK from GitHub Releases."
downloadUrl="https://github.com/you/your-app/releases"
downloadAriaLabel="Download My App APK"
/>
<!-- Google Play + web fallback -->
<DownloadSection
heading="Download Now"
playStoreUrl="https://play.google.com/store/apps/details?id=com.example"
webAppUrl="https://app.example.com"
webAppLabel="web app"
/>
Responsive screenshot carousel with optional captions. Images are lazy-loaded.
Shows one slide at a time with prev/next buttons (visible on hover) and dot indicators.
Honeypot-protected contact form with name, email, subject dropdown, and message fields.
Posts to /api/contact β wire up the Cloudflare Pages function using
@guides/workers to handle it.
Preview
Send us a message
Usage
---
import ContactForm from '@guides/ui/ContactForm.astro';
---
<ContactForm />
View source
---
interface Props {
webPageName?: string;
/** Heading level for the form title. Use 1 on the standalone /contact page,
* 2 when embedded in a homepage that already has an <h1>. Defaults to 2. */
headingLevel?: 1 | 2;
}
const { webPageName, headingLevel = 2 } = Astro.props;
const Heading = `h${headingLevel}` as 'h1' | 'h2';
const headingText = webPageName ? `Contact ${webPageName}` : 'Send us a message';
---
<section id="contact" class="contact-content">
<div class="container">
<div class="contact-single">
<div class="contact-form-section">
<Heading>{headingText}</Heading>
<form class="contact-form" method="post" action="https://formspree.io/f/xbdbakov">
{webPageName && <input type="hidden" name="site" value={webPageName} />}
<div class="form-group" style="display:none">
<label>Don't fill this out if you're human: <input name="bot-field" /></label>
</div>
<input type="hidden" name="form_start_time" />
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
</div>
<div class="form-group">
<label for="subject">Subject</label>
<select id="subject" name="subject" required>
<option value="">Select a subject</option>
<option value="bug">Bug Report</option>
<option value="feature">Feature Request</option>
<option value="help">Help & Support</option>
<option value="feedback">General Feedback</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea id="message" name="message" rows="6" required></textarea>
</div>
<input type="hidden" name="_subject" value="New contact form submission" />
<input type="hidden" name="_next" value="/thank-you" />
<button type="submit" class="submit-button">Send Message</button>
</form>
</div>
</div>
</div>
</section>
<script>
// Bots that fill and submit the form faster than a human plausibly could
// (typically instant) get silently dropped β no error shown, since a bot
// that gets feedback just adjusts its timing.
const MIN_FILL_TIME_MS = 2000;
document.querySelectorAll('form.contact-form').forEach((form) => {
const startInput = form.querySelector('input[name="form_start_time"]');
if (!startInput) return;
startInput.value = String(Date.now());
form.addEventListener('submit', (event) => {
const startedAt = Number(startInput.value);
if (startedAt && Date.now() - startedAt < MIN_FILL_TIME_MS) {
event.preventDefault();
}
});
});
</script>
π¬
ContactPage@guides/ui/ContactPage.astro
ContactForm wrapped in a <main> element. Used automatically by SiteLayout
when you pass variant="contact" β no need to import it directly in most cases.
Preview
Contact My Site
Usage
---
import Layout from '../layouts/Layout.astro';
---
<!-- Simplest approach β use the variant prop: -->
<Layout
title="Contact β My Site"
description="Get in touch."
variant="contact"
/>
---
import Layout from '../layouts/Layout.astro';
---
<Layout
title="Message Sent β My Site"
description="Your message has been received."
variant="thanks"
/>
View source
---
---
<main>
<section class="hero" style="min-height: 60vh; display: flex; align-items: center;">
<div class="hero-content">
<h1>Message sent!</h1>
<h2>Thanks for reaching out</h2>
<p>We've received your message and will get back to you soon.</p>
<a href="/" class="cta-button" style="margin-top: 1rem;">Back to Home</a>
</div>
</section>
</main>
π
PrivacyPage@guides/ui/PrivacyPage.astro
Pre-written privacy policy covering data collection, local storage, contact form, and
(when the site sets cloudflareAnalytics) Cloudflare Web Analytics. Takes the site
config; the last-updated date, storage description, and third-party links
come from its privacy block. Add extra sections via the default slot.
Preview
Privacy Policy
Last updated: May 2026
What we collect
My Site does not collect, store, or share any personal data. There are no accounts, no sign-ups, and no ads.
Local storage
The app saves your progress locally on your device. This data never leaves your device.
Contact form
If you use the contact form, your name, email address, and message are submitted for the sole purpose of replying to your enquiry. This information is not shared with third parties.
Analytics
This site uses Google Analytics 4
to understand how visitors use the site. Google Analytics may set cookies to collect anonymous usage data such as pages visited and session duration;
no personally identifiable information is collected.
See Google's Privacy Policy for details.
This site also uses Cloudflare Web Analytics,
a privacy-first analytics service that measures aggregate traffic without using cookies, without storing any personal data,
and without fingerprinting or tracking visitors across sites.
See Cloudflare's Privacy Policy for details.
---
// Layout.astro β privacy copy lives in the site config:
export const config: SiteConfig = {
// ...
privacy: {
lastUpdated: 'May 2026',
localStorageDescription: 'The app saves your progress locally. This data never leaves your device.',
thirdPartyLinks: [],
},
};
---
---
// pages/privacy.astro
import Layout, { config } from '../layouts/Layout.astro';
import PrivacyPage from '@guides/ui/PrivacyPage.astro';
---
<Layout
title={`Privacy Policy β ${config.ogSiteName}`}
description={`${config.ogSiteName} privacy policy. We don't collect personal data, serve ads, or require sign-ups.`}
noindex={true}
>
<PrivacyPage {config} />
</Layout>
View source
---
import type { SiteConfig } from './SiteLayout.astro';
interface Props {
config: SiteConfig;
contactHref?: string;
}
const { config, contactHref = '/contact' } = Astro.props;
const siteName = config.ogSiteName;
const {
lastUpdated,
localStorageDescription = 'The app saves your progress locally on your device using your browser\'s local storage. This data never leaves your device.',
thirdPartyLinks = ['Google Play'],
} = config.privacy ?? {};
// Shown only when the site's Pages project has the automatic Cloudflare Web Analytics toggle enabled.
const cloudflareAnalytics = !!config.cloudflareAnalytics;
const thirdPartyText = thirdPartyLinks.length === 1
? thirdPartyLinks[0]
: thirdPartyLinks.slice(0, -1).join(', ') + ' and ' + thirdPartyLinks.at(-1);
---
<main>
<section class="privacy-section">
<div class="container">
<h1>Privacy Policy</h1>
{lastUpdated && <p class="updated">Last updated: {lastUpdated}</p>}
<h2>What we collect</h2>
<p>{siteName} does not collect, store, or share any personal data. There are no accounts, no sign-ups, and no ads.</p>
<h2>Local storage</h2>
<p>{localStorageDescription}</p>
<h2>Contact form</h2>
<p>If you use the contact form, your name, email address, and message are submitted for the sole purpose of replying to your enquiry. This information is not shared with third parties.</p>
<h2>Analytics</h2>
<p>
This site uses <a href="https://marketingplatform.google.com/about/analytics/" target="_blank" rel="noopener noreferrer">Google Analytics 4</a>
to understand how visitors use the site. Google Analytics may set cookies to collect anonymous usage data such as pages visited and session duration;
no personally identifiable information is collected.
See <a href="https://policies.google.com/privacy" target="_blank" rel="noopener noreferrer">Google's Privacy Policy</a> for details.
</p>
{cloudflareAnalytics && (
<p>
This site also uses <a href="https://www.cloudflare.com/web-analytics/" target="_blank" rel="noopener noreferrer">Cloudflare Web Analytics</a>,
a privacy-first analytics service that measures aggregate traffic without using cookies, without storing any personal data,
and without fingerprinting or tracking visitors across sites.
See <a href="https://www.cloudflare.com/privacypolicy/" target="_blank" rel="noopener noreferrer">Cloudflare's Privacy Policy</a> for details.
</p>
)}
{thirdPartyLinks.length > 0 && (
<>
<h2>Third-party links</h2>
<p>This site links to {thirdPartyText}. Their own privacy policies apply when you visit those services.</p>
</>
)}
<slot />
<h2>Contact</h2>
<p>Questions? <a href={contactHref}>Get in touch.</a></p>
</div>
</section>
</main>
<style>
.privacy-section {
background: var(--surface);
padding: 72px 0;
}
.privacy-section .container {
max-width: 720px;
}
h1 {
font-size: clamp(1.8rem, 4vw, 2.4rem);
font-weight: 800;
color: var(--text);
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
text-align: left;
}
.updated {
font-size: 0.9rem;
color: var(--text-muted);
margin-bottom: 2rem;
}
h2 {
text-align: left;
font-size: 1.2rem;
margin-top: 2rem;
margin-bottom: 0.6rem;
}
a {
color: var(--primary);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
πΊοΈ
createSitemap()@guides/ui/sitemap
Wraps @astrojs/sitemap with sensible defaults: excludes /contact,
/thank-you, and /privacy from the generated sitemap. Pass extra
paths to exclude if needed.
Usage
// astro.config.mjs
import { defineConfig } from 'astro/config';
import { createSitemap } from '@guides/ui/sitemap';
export default defineConfig({
site: 'https://example.pages.dev',
integrations: [createSitemap()],
// Pass extra paths to exclude:
// integrations: [createSitemap(['/secret-page'])],
});
Drop-in Cloudflare Pages Function handler for the contact form. Validates fields, checks
the honeypot, and redirects to /thank-you on success. Returns 400 if required
fields are missing.
Usage
// functions/api/contact.ts
import type { EventContext } from '@cloudflare/workers-types';
import { handleContactForm } from '@guides/workers/contact';
export const onRequestPost = (
context: EventContext<Record<string, unknown>, string, Record<string, unknown>>
) => handleContactForm(context.request, 'my-new-site');