Imported from devskale/skalego (
AGENTS.md). Install upstream withnpx skills add devskale/skalego. Copyright stays with the author.
AGENTS.md — skalego / skale.dev
Production branch: main · Live: skale.dev
Deploy = ./scripts/deploy-skale.sh --build (build + sync + smoke check). Target and infrastructure details live in the script and deploy.config — deliberately NOT in this file. git push is repo backup only — it deploys nothing.
Stack
- Astro 7 (static output →
dist/) — primary build system. File-based routing fromsrc/pages/, no framework runtime (ships plain HTML + one small client script). - Astro reference:
astro_guide.md— indexed map of all canonical doc pages ondocs.astro.build. Best-practices guide (project structure, content collections, SEO, TS):../gwen.at/astro_guide.md. - Hugo (
config.toml+data/) — legacy only; not part of the live build. Thethemes/skalego_themesubmodule was removed. - pnpm lockfile present; all commands use
pnpm(not npm). - Hosting: pure static — the web server just serves
dist/.api/andvercel.jsonare legacy from a previous host; the live/s/<slug>install endpoints are static files generated at build time. - Language: German (de) content; English code/comments.
Commands
# Dev server (Astro + HMR) — runs in its own pane at http://localhost:4321
pnpm dev
# Auto-reloads on any change to src/ or public/. Do NOT launch a second dev server (port conflict).
# Production build → dist/ (also writes the static /s/<slug> install scripts)
pnpm run build
# Deploy (build + sync + smoke check) — THE deploy; details in scripts/deploy-skale.sh
./scripts/deploy-skale.sh --build
# Preview the production build locally
pnpm run preview
# Keystatic (dev-only): write/edit blog posts visually at http://localhost:4321/keystatic
# (excluded from `astro build` → production stays pure static / zero-JS). Edits write
# .md files into src/content/blog/ via local storage.
Project Structure
astro.config.mjs ← static output, site https://skale.dev, format: directory
src/
pages/ ← FILE-BASED ROUTING — one .astro per page (index, apps, agent-coding,
impressum, datenschutz, tos) + blog/ (index + [...slug]) → /apps/, /blog/<post>/
content.config.ts ← blog collection (Zod schema): title/description/date/author/tags/draft/image + optional `skills[]` (drives /skills + /s/<slug>)
content/blog/ ← one folder per post, index.md(x) — frontmatter validated by the collection
layouts/
BaseLayout.astro ← <head>/SEO/fonts + Nav + Footer + global init script + <slot name="head">
LegalLayout.astro ← wraps BaseLayout with legal-page styles + back-link
components/
Nav.astro, Footer.astro ← shared chrome; Nav has an **Agents ▾** dropdown (→ Agentic Coding + Skills)
sections/ ← Hero, Clients, Services, Process, CaseStudies, Models, FAQ, Contact
lib/authors.ts, lib/schema.ts ← blog byline + BlogPosting JSON-LD helpers
styles/global.css ← design system: tokens → base → components → layout (merged)
scripts/site.js ← the ONE client script: hero canvas + reveals + nav + mobile menu
data/site.js ← single source of truth: organization, faqs, models (drives JSON-LD too)
assets/seo/ ← SVG sources for OG image + PWA icons (rendered to PNG in public/)
api/ ← legacy functions from a previous host (not part of the static build)
public/firmenindex/ ← Firmensuche sub-app (standalone HTML/JS, copied verbatim)
public/ ← served at site root: robots.txt, site.webmanifest,
og-image.png, logos/, screenshots, client PNGs — copied verbatim to dist/
static/, data/, content/, archetypes/ ← Hugo legacy (not built)
Import aliases (tsconfig.json)
@components/*, @layouts/*, @data/*, @styles/* — prefer these over deep relative paths (../../…).
Conventions
- CSS: custom properties (
var(--red),--mono, …) live at the top ofsrc/styles/global.css. Single red accent (--red: #e53935), dark theme. Fonts: Inter / Inter Tight + JetBrains Mono — self-hosted (woff2, latin subset,font-display: swap) inpublic/fonts/, declared via@font-faceat the top ofglobal.css; preloads for the two above-fold weights inBaseLayout. No third-party font CDN (DSGVO). - Per-page styles: scoped
<style is:global>block in the page (legal-page, apps-page, ac-page). Global classes live inglobal.css. - Content stays in sync with schema: list-driven content (FAQs, models) lives in
src/data/site.jsand is imported by BOTH the visible components AND the JSON-LD, so they can never drift. - JS: zero by default. The only client JS is
src/scripts/site.js(imported once from BaseLayout, bundled+inlined by Astro). All motion is CSS-driven (IntersectionObserver reveal); honorsprefers-reduced-motion. - German content; English code/comments.
- Base URL:
skale.dev. Astrosite: 'https://skale.dev'inastro.config.mjs; canonicals computed fromAstro.site.
Build — routing (file-based, no footgun)
Astro emits a route per file in src/pages/. format: 'directory' → /apps/index.html, served as /apps/.
When adding a page: create
src/pages/<name>.astro(using BaseLayout or LegalLayout). It routes automatically — nothing to register. Thenpnpm run buildand confirm it appears indist/before pushing. (This replaces the old ViterollupOptions.inputfootgun, which is gone.)
Skills system — skale.dev/s/<slug> installs
One blog entry is the single source of truth; a build-time Astro integration turns it into static per-skill install files + a listing page. Edit one entry → build + deploy → everything updates.
The registry = one blog post: src/content/blog/recommended-skills/index.mdx
- frontmatter
skills[]array:{ slug, desc, install, hidden? }— edit this (Keystatic at/keystaticin dev, or the markdown) to add/remove/reorder a skill. Nothing else changes. installformats:pi-skill(enable one skill fromdevskale/skale-skillsvia the pi package whitelist) ·pi-skillset:a,b,c(several at once) ·command:<cmd>(arbitrary, e.g.npx impeccable install, or a multi-skillnpx skills add … --skill a b c -a codex -y).hidden: true→ off the/skills/page AND the 404 "available" list, but still installable via/s/<slug>(used for thegakefaytest skill).
Data flow:
recommended-skills.mdx (frontmatter skills[])
├── astro build → skillsStatic integration (astro.config.mjs) → dist/s/<slug>
│ static install scripts, served as plain files (hidden included;
│ dist/s/available lists the visible ones)
└── astro build → getCollection('blog') → src/pages/skills/index.astro (the /skills/ list page)
- No runtime backend: the web server serves the extensionless files directly. The script template shared with the legacy generator lives in
scripts/skills-lib.mjs. package.jsonbuild=node scripts/gen-skills-json.mjs && astro build— the gen step only emits the legacyapi/skills.registry.js(gitignored). The live/s/files come from the integration, which runs on EVERYastro build(including the deploy script's direct build call).- Add a skill: edit the blog entry's
skills[]→./scripts/deploy-skale.sh --build→ live on/skills/+/s/<slug>. - Install UX:
curl -fsSL https://skale.dev/s/<slug> | bash. Each card on/skills/has a copy-on-click icon (code scrolls internally; icon stays pinned). pi-skill/pi-skillsetinstalls edit~/.pi/agent/settings.json(idempotent whitelist-add);commandruns whatever you specify.npx skills-style bundles install into.agents/skills/(use-a codexto keep it to.agents/only — no per-agent symlinks).
SEO (keep consistent)
- Canonical domain:
skale.dev. Every canonical/OG/JSON-LD URL uses this. Do not useskale.io(decommissioned). - Static SEO files in
public/served at root:robots.txt,site.webmanifest,og-image.png, PWA icons. HARTE REGEL: kein llms.txt (nutzer-regel 2026-09-15, global — Agenten-Lesbarkeit lebt on-page, siehe firmenindex agents.html-Muster).sitemap-index.xmlis auto-generated by@astrojs/sitemap(includes every page + blog post) — there is no manual sitemap to edit. - Structured data is wired via
<Fragment slot="head">per page:index.astro→ ProfessionalService + WebSite + FAQPage (FAQ sourced fromsrc/data/site.js);apps.astro&agent-coding.astro→ BreadcrumbList + CollectionPage/SoftwareApplication;blog/index.astro→ Blog; each blog post → BlogPosting (viasrc/lib/schema.ts). Legal pages carry no JSON-LD. - Every page head (BaseLayout) carries: canonical, Open Graph, Twitter card, theme-color, robots.
- JSON-LD must stay valid. Validate after building:
pnpm run build && python3 -c "import re,json,glob;[json.loads(b) for f in glob.glob('dist/**/*.html',recursive=True) for b in re.findall(r'<script type=\"application/ld\+json\">(.*?)</script>',open(f).read(),re.S)] and print('JSON-LD OK')"
Deployment
- Deploy =
./scripts/deploy-skale.sh --build. Builds, syncsdist/to the live webroot, then smoke-checks that the live site serves the fresh build (md5 diff, exit 1 on mismatch). Target, SSH alias and paths live indeploy.config(gitignored) + the script. - git push is NOT a deploy. Commit → run the script → verify. A pushed-but-not-deployed change is never live.
- Script guards: dirty-tree abort · firmenindex files must NEVER enter this build (the app has its own webroot and own deploy — incident 2026-09-10) · stale-dist abort (source newer than build → use
--build). - Verify after deploy:
curl -sI https://skale.dev/<file>(200) andcurl -s https://skale.dev/s/fetch-url | head -3(install script). - Target sidefiles — agents: read the one matching the live target before touching server config or endpoints:
deploy.oci.md— ACTIVE: OCI VM, self-hosted nginx, pure static.deploy.vercel.md— RETIRED: history only; don't deploy there, endpoints documented there are dead.
Infrastructure details (server, paths, redirects, proxies) belong in the sidefiles + deploy.config/scripts/deploy-skale.sh — keep this file deploy-agnostic. Redirects/rewrites live in web-server config on the target, not in this repo.
Boundaries
- Never modify:
node_modules/,dist/,.astro/ - Never commit: secrets, API keys, tokens
- Ask before: changing canonical domain, adding API endpoints, changing redirects/rewrites in the web-server config on the deploy target
- Safe without asking: CSS tweaks, content copy edits, adding sections/pages, JS animation changes
E2E Validation (rodney)
Use rodney (headless Chrome) for visual checks and smoke tests against the dev server:
rodney start
rodney open http://localhost:4321
rodney waitstable
rodney screenshot research/screenshot-<feature>.png
rodney stop # ALWAYS stop when done
Screenshots go in ./research/ (gitignored). Rodney skill: ~/.pi/agent/skills/rodney/SKILL.md.
Known Footguns
- Deploy ist der Script, nicht git push.
./scripts/deploy-skale.sh --buildnach jedem Commit, der live gehen soll. - Pure static. No SSR adapter, no runtime backend —
/s/<slug>is static files; the web server only serves files.api/+vercel.jsonare legacy — don't extend them, don't point users at endpoints that rely on a runtime. - No
pnpm-workspace.yaml. The lockfile is pnpm@9; pnpm 9 errors (packages field missing or empty) on a workspace file that lacks apackages:field, and the pnpm-10+allowBuildssyntax is incompatible. (Locally pnpm 11 prints an "ignored build scripts" warning on fresh install — harmless, esbuild/sharp aren't invoked at build since we don't useastro:assets.) public/firmenindex/is a standalone sub-app (own HTML/JS, query-param routing) copied verbatim. It is NOT an Astro page — edit its files directly underpublic/.- Hugo legacy (
config.toml,data/,static/,content/) coexists but is never built. Changing it does nothing to the live site. - The single client script is
src/scripts/site.js(hero particle canvas + scroll-reveal + nav + mobile menu). Astro inlines small scripts; checkdist/if a feature seems missing. - Blog = Astro Content Collections (
src/content.config.ts→blog). Add a post by creatingsrc/content/blog/<slug>/index.md(x)with validated frontmatter; it auto-routes to/blog/<slug>/. After adding an integration or a content collection, restart the dev server (pnpm dev) — a running dev pane won't pick up new integrations (MDX/sitemap) or collections until restarted, so new routes 404 in dev while still building fine. - German legal requirements: Impressum (§ 5 TMG), Datenschutzerklärung (DSGVO), AGB (
tos.astro) must stay accurate and accessible — all real Astro routes (/impressum/,/datenschutz/,/tos/). research/,updateplan.md,skills-lock.json,tests/,screenshots/,api/skills.registry.jsare gitignored — don't commit them. (skills.registry.jsis generated by the prebuild from the blog entry.)