Sub-skills
SEO and feeds in depth
For your agentCopy this and give it to the AI you're building with.
Run `cubek skill`, then `cubek skill seo`, read all of both and follow them exactly (no CLI: download https://cubek.dev/SKILL.md and https://cubek.dev/skills/seo.md verbatim with curl; don't summarize them). Run `cubek check` after every edit until it is clean.
Search engines, social cards and feeds for server-rendered pages: meta tags, canonical URLs, OpenGraph, JSON-LD, Atom/RSS, sitemap, robots.txt.
// schema.ts
import { table, t } from "cubek"
export const posts = table("posts", { id: t.id(), slug: t.string().unique(), title: t.string(), excerpt: t.string(), body: t.string(), author: t.string(), createdAt: t.timestamp().default("now") }).index("createdAt")// src/app.tsx
import { app, db, jsonLd, markdown, raw } from "cubek"
const xml = (s: string): string => s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """)
app.get("/p/:slug", async (c) => {
const post = await db.posts.findOne({ where: { slug: c.param("slug") } })
if (post === null) return c.notFound()
const url = `${c.origin()}/p/${post.slug}`
return c.render(
<html lang="en">
<head>
<meta charset="utf-8" /><title>{post.title}</title>
<meta name="description" content={post.excerpt} /><link rel="canonical" href={url} />
<meta property="og:type" content="article" /><meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} /><meta property="og:url" content={url} />
<link rel="alternate" type="application/atom+xml" href="/feed.xml" />
{jsonLd({ "@context": "https://schema.org", "@type": "BlogPosting", headline: post.title, datePublished: post.createdAt, author: { "@type": "Person", name: post.author } })}
</head>
<body><article><h1>{post.title}</h1>{raw(markdown.render(post.body))}</article></body>
</html>,
)
})
app.get("/feed.xml", async (c) => {
const o = c.origin()
const list = await db.posts.find({ orderBy: { createdAt: "desc" }, limit: 20 })
const entries = list.map((p) => `<entry><title>${xml(p.title)}</title><link href="${o}/p/${p.slug}"/><id>${o}/p/${p.slug}</id><updated>${p.createdAt}</updated><author><name>${xml(p.author)}</name></author><content type="html">${xml(markdown.html(p.body))}</content></entry>`)
const feed = `<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title>Blog</title><id>${o}/</id><link href="${o}/"/><updated>${list.length > 0 ? list[0].createdAt : new Date().toISOString()}</updated>${entries.join("")}</feed>`
return c.text(feed, 200, { "content-type": "application/atom+xml; charset=utf-8" })
})
app.get("/sitemap.xml", async (c) => {
const list = await db.posts.find({ orderBy: { createdAt: "desc" }, limit: 1000 })
const urls = list.map((p) => `<url><loc>${c.origin()}/p/${p.slug}</loc><lastmod>${p.createdAt}</lastmod></url>`)
return c.text(`<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls.join("")}</urlset>`, 200, { "content-type": "application/xml" })
})
app.get("/robots.txt", async (c) => c.text(`User-agent: *\nAllow: /\nSitemap: ${c.origin()}/sitemap.xml\n`))markdown.html(md)is the sanitized HTML as a string (feeds, mailhtml);markdown.renderisSafeHtmlforraw()in templates. Interpolating aSafeHtmlor JSX into a string is a compile error.- Content types: Atom
application/atom+xml, RSSapplication/rss+xml, sitemapapplication/xml, robotstext/plain(thec.textdefault). - Atom takes the ISO timestamps as they are. RSS 2.0 needs RFC 822 dates
(
Tue, 01 Sep 2026 10:00:00 GMT, built by hand fromtoISOString()) and the HTML escaped as above:<description>${xml(markdown.html(p.body))}</description>. - Absolute URLs from
c.origin()(canonical,og:url,og:image, feeds). jsonLd(data)escapes the JSON; never a<script>with inline content.- Social cards:
og:image1200x630 inpublic/(c.origin() + asset("og.png")) and<meta name="twitter:card" content="summary_large_image" />. - A page rendered to a string:
await (await c.render(<Page />)).text().