Webhooks for the AT Protocol airglow.run
atproto atprotocol automation webhook
12
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: seo improvements

Hugo 2460099a 7703b88f

+214 -45
+2
app/global.d.ts
··· 3 3 4 4 type Head = { 5 5 title?: string; 6 + description?: string; 7 + ogImage?: string; 6 8 }; 7 9 8 10 declare module "hono" {
+28 -2
app/routes/_renderer.tsx
··· 1 1 import { jsxRenderer } from "hono/jsx-renderer"; 2 2 import { raw } from "hono/html"; 3 3 import { Script } from "honox/server"; 4 + import { config } from "@/config.js"; 4 5 import "../styles/reset.css.js"; 5 6 import "../styles/theme.css.js"; 6 7 import "../styles/global.css.js"; ··· 47 48 ); 48 49 } 49 50 50 - export default jsxRenderer(({ children, title }) => { 51 + const defaultDescription = 52 + "Automate the AT Protocol and Bluesky — set up webhooks, create records, and filter Jetstream events by lexicon."; 53 + const defaultOgImage = `${config.publicUrl}/og-image.png`; 54 + 55 + export default jsxRenderer(({ children, title, description, ogImage }, c) => { 56 + const desc = description ?? defaultDescription; 57 + const og = ogImage ?? defaultOgImage; 58 + const pageTitle = title ?? "Airglow"; 59 + const canonicalUrl = `${config.publicUrl}${c.req.path}`; 60 + 51 61 return ( 52 62 <html lang="en"> 53 63 <head> ··· 57 67 {!import.meta.env.PROD && <link rel="stylesheet" href="/__dev.css" />} 58 68 <CssLinks /> 59 69 <link rel="icon" href="/favicon.svg" type="image/svg+xml" /> 60 - <title>{title ?? "Airglow"}</title> 70 + <link rel="canonical" href={canonicalUrl} /> 71 + <title>{pageTitle}</title> 72 + <meta name="description" content={desc} /> 73 + <meta name="theme-color" content="#1e1e1e" media="(prefers-color-scheme: dark)" /> 74 + <meta name="theme-color" content="#f9f7f1" media="(prefers-color-scheme: light)" /> 75 + {/* Open Graph */} 76 + <meta property="og:type" content="website" /> 77 + <meta property="og:url" content={canonicalUrl} /> 78 + <meta property="og:title" content={pageTitle} /> 79 + <meta property="og:description" content={desc} /> 80 + <meta property="og:image" content={og} /> 81 + <meta property="og:site_name" content="Airglow" /> 82 + {/* Twitter card */} 83 + <meta name="twitter:card" content="summary_large_image" /> 84 + <meta name="twitter:title" content={pageTitle} /> 85 + <meta name="twitter:description" content={desc} /> 86 + <meta name="twitter:image" content={og} /> 61 87 <Script src="/app/client.ts" async /> 62 88 </head> 63 89 <body>{children}</body>
+5 -1
app/routes/auth/login.tsx
··· 62 62 </div> 63 63 </Container> 64 64 </AppShell>, 65 - { title: "Sign in — Airglow" }, 65 + { 66 + title: "Sign in — Airglow", 67 + description: 68 + "Sign in to Airglow with your AT Protocol identity to create automations and webhooks.", 69 + }, 66 70 ); 67 71 }); 68 72
+62 -40
app/routes/index.tsx
··· 1 1 import { createRoute } from "honox/factory"; 2 2 import { desc, count } from "drizzle-orm"; 3 + import { raw } from "hono/html"; 3 4 import { Webhook, FilePlus2, Filter, Activity } from "../icons.js"; 4 5 import { getSessionUser } from "@/auth/middleware.js"; 5 6 import { db } from "@/db/index.js"; ··· 13 14 import ThemeToggle from "../islands/ThemeToggle.js"; 14 15 import * as s from "../styles/pages/landing.css.js"; 15 16 17 + const jsonLd = raw( 18 + `<script type="application/ld+json">${JSON.stringify({ 19 + "@context": "https://schema.org", 20 + "@type": "WebApplication", 21 + name: "Airglow", 22 + url: "https://airglow.run", 23 + description: 24 + "Automate the AT Protocol and Bluesky — webhooks, record creation, and Jetstream event filtering.", 25 + applicationCategory: "DeveloperApplication", 26 + operatingSystem: "Web", 27 + })}</script>`, 28 + ); 29 + 16 30 export default createRoute(async (c) => { 17 31 const user = await getSessionUser(c); 18 32 ··· 25 39 26 40 return c.render( 27 41 <AppShell header={<Header user={user} actions={<ThemeToggle />} />}> 42 + {jsonLd} 28 43 <Container> 29 44 <section class={s.hero}> 30 - <h1 class={s.heroTitle}>Automations for the AT Protocol</h1> 45 + <h1 class={s.heroTitle}>Webhooks &amp; Automations for the AT Protocol</h1> 31 46 <p class={s.heroSubtitle}> 32 - Listen to events across the AT Protocol network and trigger actions automatically. 33 - Filter by lexicon, deliver webhooks, create records on your PDS, and track every run. 47 + Automate Bluesky and the AT Protocol network. Listen to Jetstream events, filter by 48 + lexicon, deliver webhooks, create records on your PDS, and track every run. 34 49 </p> 35 50 {user ? ( 36 51 <Button href="/dashboard" size="lg"> ··· 43 58 )} 44 59 </section> 45 60 46 - <section class={s.features}> 47 - <div class={s.featureCard}> 48 - <div class={s.featureIcon}> 49 - <Webhook size={28} /> 61 + <section> 62 + <h2 class={s.stepsTitle}>Features</h2> 63 + <div class={s.features}> 64 + <div class={s.featureCard}> 65 + <div class={s.featureIcon}> 66 + <Webhook size={28} /> 67 + </div> 68 + <h3 class={s.featureTitle}>Webhook Delivery</h3> 69 + <p class={s.featureDesc}> 70 + Receive HTTP POST callbacks instantly when matching events occur on the AT Protocol 71 + network via Jetstream. 72 + </p> 50 73 </div> 51 - <h3 class={s.featureTitle}>Webhook Delivery</h3> 52 - <p class={s.featureDesc}> 53 - Receive HTTP POST callbacks instantly when matching events occur on the AT Protocol 54 - network via Jetstream. 55 - </p> 56 - </div> 57 - <div class={s.featureCard}> 58 - <div class={s.featureIcon}> 59 - <FilePlus2 size={28} /> 74 + <div class={s.featureCard}> 75 + <div class={s.featureIcon}> 76 + <FilePlus2 size={28} /> 77 + </div> 78 + <h3 class={s.featureTitle}>Record Creation</h3> 79 + <p class={s.featureDesc}> 80 + Automatically create records on your PDS when events match. Use templates with 81 + placeholders to build records from event data. 82 + </p> 60 83 </div> 61 - <h3 class={s.featureTitle}>Record Creation</h3> 62 - <p class={s.featureDesc}> 63 - Automatically create records on your PDS when events match. Use templates with 64 - placeholders to build records from event data. 65 - </p> 66 - </div> 67 - <div class={s.featureCard}> 68 - <div class={s.featureIcon}> 69 - <Filter size={28} /> 84 + <div class={s.featureCard}> 85 + <div class={s.featureIcon}> 86 + <Filter size={28} /> 87 + </div> 88 + <h3 class={s.featureTitle}>Smart Filtering</h3> 89 + <p class={s.featureDesc}> 90 + Listen to specific record types by NSID. Add field-level conditions with operators 91 + like equals, starts with, or contains. 92 + </p> 70 93 </div> 71 - <h3 class={s.featureTitle}>Smart Filtering</h3> 72 - <p class={s.featureDesc}> 73 - Listen to specific record types by NSID. Add field-level conditions with operators 74 - like equals, starts with, or contains. 75 - </p> 76 - </div> 77 - <div class={s.featureCard}> 78 - <div class={s.featureIcon}> 79 - <Activity size={28} /> 94 + <div class={s.featureCard}> 95 + <div class={s.featureIcon}> 96 + <Activity size={28} /> 97 + </div> 98 + <h3 class={s.featureTitle}>Delivery Tracking</h3> 99 + <p class={s.featureDesc}> 100 + Full delivery log with status codes, retry attempts, and error details. Know exactly 101 + what happened with every event. 102 + </p> 80 103 </div> 81 - <h3 class={s.featureTitle}>Delivery Tracking</h3> 82 - <p class={s.featureDesc}> 83 - Full delivery log with status codes, retry attempts, and error details. Know exactly 84 - what happened with every event. 85 - </p> 86 104 </div> 87 105 </section> 88 106 ··· 139 157 </section> 140 158 </Container> 141 159 </AppShell>, 142 - { title: "Airglow — Automations for the AT Protocol" }, 160 + { 161 + title: "Airglow — Webhooks & Automations for the AT Protocol", 162 + description: 163 + "Automate Bluesky and the AT Protocol. Set up webhooks, create records, and filter Jetstream events by lexicon — no code required.", 164 + }, 143 165 ); 144 166 });
+6 -1
app/routes/lexicons/[nsid].tsx
··· 178 178 </Stack> 179 179 </Container> 180 180 </AppShell>, 181 - { title: `${nsid} — Airglow` }, 181 + { 182 + title: `${nsid} — Airglow`, 183 + description: description 184 + ? `${description.replace(/\.?$/, ".")} Browse automations for ${nsid} on Airglow.` 185 + : `Browse automations using the ${nsid} AT Protocol lexicon on Airglow.`, 186 + }, 182 187 ); 183 188 });
+63
app/routes/lexicons/index.tsx
··· 1 + import { createRoute } from "honox/factory"; 2 + import { desc, count } from "drizzle-orm"; 3 + import { getSessionUser } from "@/auth/middleware.js"; 4 + import { db } from "@/db/index.js"; 5 + import { automations } from "@/db/schema.js"; 6 + import { AppShell } from "../../components/Layout/AppShell/index.js"; 7 + import { Header } from "../../components/Layout/Header/index.js"; 8 + import { Container } from "../../components/Layout/Container/index.js"; 9 + import { PageHeader } from "../../components/Layout/PageHeader/index.js"; 10 + import { Table } from "../../components/Table/index.js"; 11 + import { InlineCode } from "../../components/CodeBlock/index.js"; 12 + import ThemeToggle from "../../islands/ThemeToggle.js"; 13 + 14 + export default createRoute(async (c) => { 15 + const user = await getSessionUser(c); 16 + 17 + const lexicons = await db 18 + .select({ lexicon: automations.lexicon, count: count() }) 19 + .from(automations) 20 + .groupBy(automations.lexicon) 21 + .orderBy(desc(count())); 22 + 23 + return c.render( 24 + <AppShell header={<Header user={user} actions={<ThemeToggle />} />}> 25 + <Container> 26 + <PageHeader 27 + title="AT Protocol Lexicons" 28 + description={`Browse ${lexicons.length} lexicon${lexicons.length !== 1 ? "s" : ""} with active automations on Airglow.`} 29 + /> 30 + 31 + {lexicons.length > 0 ? ( 32 + <Table> 33 + <thead> 34 + <tr> 35 + <th>NSID</th> 36 + <th>Automations</th> 37 + </tr> 38 + </thead> 39 + <tbody> 40 + {lexicons.map((row) => ( 41 + <tr key={row.lexicon}> 42 + <td> 43 + <a href={`/lexicons/${row.lexicon}`}> 44 + <InlineCode>{row.lexicon}</InlineCode> 45 + </a> 46 + </td> 47 + <td>{row.count}</td> 48 + </tr> 49 + ))} 50 + </tbody> 51 + </Table> 52 + ) : ( 53 + <p>No lexicons with active automations yet.</p> 54 + )} 55 + </Container> 56 + </AppShell>, 57 + { 58 + title: "AT Protocol Lexicons — Airglow", 59 + description: 60 + "Browse AT Protocol lexicons with active automations on Airglow. Discover webhooks and automations for Bluesky and the AT Protocol network.", 61 + }, 62 + ); 63 + });
+4 -1
app/routes/u/[handle]/index.tsx
··· 215 215 </Stack> 216 216 </Container> 217 217 </AppShell>, 218 - { title: `@${profileUser?.handle ?? handle} — Airglow` }, 218 + { 219 + title: `@${profileUser?.handle ?? handle} — Airglow`, 220 + description: `See @${profileUser?.handle ?? handle}'s automations and lexicons on Airglow — automation platform for the AT Protocol.`, 221 + }, 219 222 ); 220 223 });
+37
app/server.ts
··· 1 1 import { createApp } from "honox/server"; 2 2 import { createMiddleware } from "hono/factory"; 3 + import { sql } from "drizzle-orm"; 3 4 import { getOAuthClient } from "@/auth/client.js"; 5 + import { config } from "@/config.js"; 6 + import { db } from "@/db/index.js"; 7 + import { automations, users } from "@/db/schema.js"; 4 8 import { startJetstream } from "@/jetstream/consumer.js"; 5 9 import { handleMatchedEvent } from "@/jetstream/handler.js"; 6 10 import { rateLimit } from "@/rate-limit.js"; ··· 55 59 app.get("/oauth/jwks.json", async (c) => { 56 60 const client = await getOAuthClient(); 57 61 return c.json(client.jwks); 62 + }); 63 + 64 + // Dynamic sitemap 65 + app.get("/sitemap.xml", async (c) => { 66 + const base = config.publicUrl; 67 + 68 + const lexiconRows = await db.selectDistinct({ lexicon: automations.lexicon }).from(automations); 69 + const handleRows = await db 70 + .select({ handle: users.handle }) 71 + .from(users) 72 + .where(sql`EXISTS (SELECT 1 FROM automations WHERE automations.did = users.did)`); 73 + 74 + const urls = [ 75 + ` <url><loc>${base}/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>`, 76 + ` <url><loc>${base}/lexicons</loc><changefreq>daily</changefreq><priority>0.8</priority></url>`, 77 + ...lexiconRows.map( 78 + (r) => 79 + ` <url><loc>${base}/lexicons/${r.lexicon}</loc><changefreq>weekly</changefreq><priority>0.6</priority></url>`, 80 + ), 81 + ...handleRows.map( 82 + (r) => 83 + ` <url><loc>${base}/u/${r.handle}</loc><changefreq>weekly</changefreq><priority>0.5</priority></url>`, 84 + ), 85 + ]; 86 + 87 + const xml = [ 88 + `<?xml version="1.0" encoding="UTF-8"?>`, 89 + `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`, 90 + ...urls, 91 + `</urlset>`, 92 + ].join("\n"); 93 + 94 + return c.body(xml, 200, { "Content-Type": "application/xml" }); 58 95 }); 59 96 60 97 export default app;
+7
public/robots.txt
··· 1 + User-agent: * 2 + Allow: / 3 + Disallow: /dashboard 4 + Disallow: /api/ 5 + Disallow: /auth/ 6 + 7 + Sitemap: https://airglow.run/sitemap.xml