Ship-Ready Web Essentials: Search, Sitemap, Metadata & Icons (SvelteKit)
This guide is a pragmatic, production-minded checklist and how-to for hardening a SvelteKit landing site so it’s discoverable by search engines, understandable by AI crawlers, and delightful on modern devices. It includes file locations, example contents, and verification steps. Everything here works even if you only have a single landing page today.
Branding note: this document uses a fictional brand (“Example Store”) and domain (example.com). Replace these with your own values when you implement.
Quick To-Do Checklist
• Serve a real XML sitemap at /sitemap.xml and reference it in robots.txt.
• Add a lightweight, server-rendered /search?q=… endpoint (backs JSON-LD SearchAction).
• Place ai.txt (allow/deny AI crawlers) and .well-known/security.txt (vuln-reporting).
• Wire canonical Open Graph/Twitter tags and og.png for social previews.
• Add platform icons (apple-touch-icon.png, safari-pinned-tab.svg) and site.webmanifest.
• Harden app.html with meta theme-color (light/dark), color-scheme, preconnects, canonical, Organization and WebSite JSON-LD, and a noscript fallback.
• Keep robots.txt permissive (or restrictive) as desired; always include Sitemap line.
- Verify with Google Search Console, Rich Results Test, and mobile devices.
Project Layout (UI repo)
Below is a typical SvelteKit layout. Create any missing files and drop assets into /static so they’re served as-is:
ui/
├─ src/
│ ├─ app.html
│ ├─ lib/
│ │ └─ search/
│ │ └─ links.js
│ └─ routes/
│ └─ search/
│ ├─ +page.server.js
│ └─ +page.svelte
└─ static/
├─ sitemap.xml
├─ robots.txt
├─ ai.txt
├─ og.png
├─ apple-touch-icon.png
├─ safari-pinned-tab.svg
├─ site.webmanifest
└─ .well-known/
└─ security.txt
1) Lightweight site search (/search)
Benefit: gives users a helpful quick-links search, and lets JSON-LD’s SearchAction point to a real endpoint that LLMs and search engines can resolve.
Files to add: src/lib/search/links.js, src/routes/search/+page.server.js, src/routes/search/+page.svelte
src/lib/search/links.js
export const LINKS = [
{ title: 'Home', url: '/', keywords: ['example store', 'shop', 'deals'] },
{ title: 'Sitemap', url: '/sitemap.xml', keywords: ['sitemap', 'index'] },
{ title: 'Robots', url: '/robots.txt', keywords: ['robots', 'crawl'] },
{ title: 'AI Permissions', url: '/ai.txt', keywords: ['ai', 'llm', 'gptbot'] },
{ title: 'Security Policy', url: '/.well-known/security.txt', keywords: ['security', 'vulnerability', 'report'] }
];
src/routes/search/+page.server.js
import { LINKS } from '$lib/search/links.js';
export async function load({ url }) {
const q = (url.searchParams.get('q') || '').trim();
if (!q) return { q, results: [] };
const needle = q.toLowerCase();
const results = LINKS.filter((l) =>
l.title.toLowerCase().includes(needle) ||
(l.keywords || []).some((k) => k.toLowerCase().includes(needle))
);
return { q, results };
}
src/routes/search/+page.svelte
<script>
export let data;
import { onMount } from 'svelte';
onMount(() => {
if (typeof plausible === 'function' && data?.q) {
plausible('Search', { props: { q: data.q, results: data.results?.length || 0 } });
}
});
</script>
<svelte:head>
<title>Search – Example Store</title>
<link rel="canonical" href={'https://example.com/search' + (data?.q ? `?q=${encodeURIComponent(data.q)}` : '')} />
{#if !data?.q}<meta name="robots" content="noindex,follow" />{/if}
</svelte:head>
<main class="min-h-screen container mx-auto p-6 text-white">
<form role="search" method="GET" action="/search" class="max-w-xl mx-auto mb-6 flex gap-2">
<input class="input input-bordered w-full" name="q" placeholder="Search Example Store" value={data?.q || ''} aria-label="Search" />
<button class="btn btn-primary" type="submit">Search</button>
</form>
{#if data?.q && (!data.results || data.results.length === 0)}
<p class="text-center opacity-80">No quick links matched. Try a full web search:</p>
<p class="text-center mt-2">
<a class="link" href={`https://www.google.com/search?q=site%3Aexample.com+${encodeURIComponent(data.q)}`}>Google: site:example.com {data.q}</a>
</p>
{/if}
{#if data?.results && data.results.length}
<ul class="max-w-xl mx-auto mt-4 space-y-3">
{#each data.results as r}
<li class="card bg-base-200">
<a class="card-body" href={r.url}>
<span class="card-title">{r.title}</span>
<span class="opacity-70 text-sm">{r.url}</span>
</a>
</li>
{/each}
</ul>
{/if}
</main>
2) app.html enhancements (SEO, social, performance)
Benefits: better social cards, device UI coloring, crawl hints, and faster paint. Place in src/app.html.
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#CBA0AE">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#2A202E">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="canonical" href="https://example.com/">
<!-- Open Graph / Twitter -->
<meta property="og:site_name" content="Example Store">
<meta property="og:type" content="website">
<meta property="og:title" content="Example Store — Discover great products and deals">
<meta property="og:description" content="Shop curated products with fast checkout and great support.">
<meta property="og:url" content="https://example.com/">
<meta property="og:image" content="https://example.com/og.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Example Store — Discover great products and deals">
<meta name="twitter:description" content="Shop curated products with fast checkout and great support.">
<meta name="twitter:image" content="https://example.com/og.png">
<!-- JSON-LD: Organization -->
<script type="application/ld+json">
{
"@context":"https://schema.org",
"@type":"Organization",
"name":"Example Store",
"url":"https://example.com/",
"logo":"https://example.com/apple-touch-icon.png",
"sameAs":["https://www.instagram.com/example/","https://bsky.app/profile/example.com","https://www.linkedin.com/company/example/","https://www.pinterest.com/example/"]
}
</script>
<!-- JSON-LD: WebSite with SearchAction -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"url": "https://example.com/",
"name": "Example Store",
"potentialAction": {
"@type": "SearchAction",
"target": "https://example.com/search?q={query}",
"query-input": "required name=query"
}
}
</script>
<noscript><p>Example Store works best with JavaScript enabled.</p></noscript>
3) sitemap.xml
Benefits: tells crawlers what to index and when it changed. Place at static/sitemap.xml.
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com/</loc>
<lastmod>2025-08-26</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
</urlset>
4) robots.txt
Benefits: canonical crawler permissions and a pointer to your sitemap. Place at static/robots.txt.
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml
5) ai.txt (experimental LLM permissions)
Benefits: explicit allow/deny signals for popular AI crawlers. Not a formal standard, but widely respected. Place at static/ai.txt.
User-agent: GPTBot
Allow: /
User-agent: CCBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Applebot-Image
Allow: /
User-agent: Google-Extended
Allow: /
User-agent: *
Allow: /
6) .well-known/security.txt
Benefits: standardized vulnerability reporting channel that security researchers check first. Place at static/.well-known/security.txt.
Contact: mailto:security@example.com
Policy: https://example.com/security
Preferred-Languages: en
Expires: 2026-12-31T23:59:59Z
7) og.png (social preview)
Benefits: consistent share cards across platforms. Recommended 1200×630 PNG/WebP with safe text margins. Place at static/og.png and reference via og:image and twitter:image.
8) Icons & PWA Manifest
Benefits: crisp icons on iOS/Android, pinned tabs, and installability. Place in static/.
• apple-touch-icon.png (180×180 or larger, square, no transparency).
• safari-pinned-tab.svg (monochrome vector for Safari pinned tabs).
• site.webmanifest (controls app name, icons, theme/background colors).
Example site.webmanifest (static/site.webmanifest):
{
"name": "Example Store",
"short_name": "Example",
"start_url": "/",
"display": "standalone",
"background_color": "#2A202E",
"theme_color": "#2A202E",
"icons": [
{ "src": "/apple-touch-icon.png", "sizes": "180x180", "type": "image/png", "purpose": "any maskable" }
]
}
9) Verification & QA
• Open https://example.com/robots.txt and https://example.com/sitemap.xml in a browser — ensure plain text/XML.
• View page source for app.html meta/JSON-LD; run Google’s Rich Results Test on the landing URL.
• Toggle OS Light/Dark and verify the address bar color on a real phone (Android/iOS).
• Hit https://example.com/search?q=robots — confirm SSR results.
• Submit sitemap in Google Search Console → Sitemaps; ensure no HTML-served errors.
- Check link previews by pasting the URL in social apps; confirm og.png renders.
Taylor Swift
“I want to be defined by the things that I love, not the things I hate.”
Comments
Post a Comment