{
"path": "/a/3mkydjpwul723-offprint-as-an-astro-cms",
"site": "at://did:plc:3u26lcxyhiyq3ygsfyrc7xx2/site.standard.publication/3mjxlpoh6gs2z",
"$type": "site.standard.document",
"title": "Offprint as an Astro CMS",
"content": {
"$type": "app.offprint.content",
"items": [
{
"$type": "app.offprint.block.text",
"facets": [
{
"index": {
"byteEnd": 88,
"byteStart": 76
},
"features": [
{
"uri": "https://offprint.app",
"$type": "app.offprint.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 99,
"byteStart": 90
},
"features": [
{
"uri": "https://pckt.blog",
"$type": "app.offprint.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 112,
"byteStart": 101
},
"features": [
{
"uri": "https://leaflet.pub",
"$type": "app.offprint.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 164,
"byteStart": 151
},
"features": [
{
"uri": "https://standard.site",
"$type": "app.offprint.richtext.facet#link"
}
]
}
],
"plaintext": "I've been slowly making my website more and more Atmospheric. Services like offprint.app, pckt.blog, leaflet.pub, and others have all began to use the standard.site lexicon to give a consistent lexicon for referencing these documents."
},
{
"$type": "app.offprint.block.text",
"plaintext": "But each of the apps supports different content blocks and different rendering tasks. There is no shared schema for rendering of documents. The central lexicon is primarily for making posts referenceable across the Atmosphere."
},
{
"$type": "app.offprint.block.text",
"plaintext": "I really wanted to use one of these services (or maybe several of these services) as a CMS of sorts. Where I could post on Offprint and have it be mirrored on my website."
},
{
"$type": "app.offprint.block.text",
"facets": [
{
"index": {
"byteEnd": 27,
"byteStart": 12
},
"features": [
{
"uri": "https://github.com/chrisvander/at-astro-loader",
"$type": "app.offprint.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 50,
"byteStart": 35
},
"features": [
{
"$type": "app.offprint.richtext.facet#code"
}
]
}
],
"plaintext": "So, I built a small package called at-astro-loader. We can just build things, I guess!"
},
{
"$type": "app.offprint.block.heading",
"level": 1,
"plaintext": "First Schema"
},
{
"$type": "app.offprint.block.text",
"plaintext": "On my website, I have a projects section, which looks like this:"
},
{
"$type": "app.offprint.block.image",
"image": {
"$type": "blob",
"ref": {
"$link": "bafkreig2bdfcilnz7kgilydmnc4h3zuco5blvydv555cx3hb5ii5wyxm7a"
},
"mimeType": "image/jpeg",
"size": 115436
},
"alignment": "center",
"aspectRatio": {
"width": 1394,
"height": 800
}
},
{
"$type": "app.offprint.block.text",
"facets": [
{
"index": {
"byteEnd": 149,
"byteStart": 123
},
"features": [
{
"$type": "app.offprint.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 203,
"byteStart": 190
},
"features": [
{
"uri": "https://pdsls.dev/at://did:plc:3u26lcxyhiyq3ygsfyrc7xx2/com.atproto.lexicon.schema/com.chrisvanderloo.project",
"$type": "app.offprint.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 241,
"byteStart": 213
},
"features": [
{
"uri": "https://atproto.com/guides/publishing-lexicons",
"$type": "app.offprint.richtext.facet#link"
}
]
}
],
"plaintext": "To make my website more Atmospheric, I wanted these projects to be referenced from my PDS. So, I defined a custom lexicon, com.chrisvanderloo.project, with a simple schema that you can find here on PDSLS. There's a great guide on atproto.com that helps with the basics of publishing your first schema."
},
{
"$type": "app.offprint.block.text",
"facets": [
{
"index": {
"byteEnd": 33,
"byteStart": 30
},
"features": [
{
"uri": "https://github.com/bluesky-social/atproto/tree/main/packages/lex/lex",
"$type": "app.offprint.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 126,
"byteStart": 68
},
"features": [
{
"$type": "app.offprint.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 161,
"byteStart": 149
},
"features": [
{
"$type": "app.offprint.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 184,
"byteStart": 175
},
"features": [
{
"$type": "app.offprint.richtext.facet#code"
}
]
}
],
"plaintext": "After publishing that, I used lex to install my new lexicon. I used npx -p @atproto/lex lex install com.chrisvanderloo.project . Then, I generated a src/lexicons folder using lex build . Finally, I had a type-safe schema to reference."
},
{
"$type": "app.offprint.block.text",
"facets": [
{
"index": {
"byteEnd": 40,
"byteStart": 23
},
"features": [
{
"$type": "app.offprint.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 193,
"byteStart": 178
},
"features": [
{
"$type": "app.offprint.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 330,
"byteStart": 326
},
"features": [
{
"$type": "app.offprint.richtext.facet#code"
}
]
}
],
"plaintext": "In Astro, you define a content.config.ts file to create content collections. I wanted my projects to be loaded as a content collection so I could reference it at build. Using my at-astro-loader package, it's pretty easy to set up a content collection based on an installed schema. I'm having it pull straight from my PDS, and repo references my username."
},
{
"code": "import { defineCollection } from \"astro:content\";\nimport * as com from \"./lexicons/com\";\nimport { atLoader } from \"at-astro-loader\";\n\nconst projects = defineCollection({\n loader: atLoader(com.chrisvanderloo.project, {\n endpoint: \"https://bsky.chrisvanderloo.com\",\n repo: \"chrisvanderloo.com\",\n }),\n});\n\n\nexport const collections = {\n projects,\n};",
"$type": "app.offprint.block.codeBlock",
"language": "ts"
},
{
"$type": "app.offprint.block.text",
"plaintext": " The above configuration pulls my projects at build time, easy peasy."
},
{
"$type": "app.offprint.block.heading",
"level": 1,
"plaintext": "Offprint Posts"
},
{
"$type": "app.offprint.block.text",
"plaintext": "But now, how do I do this with Offprint posts? I have articles I've written in Markdown, and those are turned into pages at build time. I'd like it to pull Offprint posts as well."
},
{
"$type": "app.offprint.block.text",
"facets": [
{
"index": {
"byteEnd": 29,
"byteStart": 14
},
"features": [
{
"$type": "app.offprint.richtext.facet#code"
}
]
}
],
"plaintext": "With 1.2.1 of at-astro-loader, I added support for Offprint documents. The loader is used as follows:"
},
{
"code": "import { defineCollection } from \"astro:content\";\nimport { atLoader, defineStandardSiteDocumentRenderer } from \"at-astro-loader\";\nimport { site } from \"at-astro-loader/lexicons\";\n\nconst atprotoBlogCollection = defineCollection({\n loader: atLoader(site.standard.document, {\n endpoint: \"https://bsky.chrisvanderloo.com\",\n repo: \"chrisvanderloo.com\",\n renderer: defineStandardSiteDocumentRenderer({\n shikiConfig: {\n themes: {\n light: \"catppuccin-latte\",\n dark: \"catppuccin-mocha\",\n },\n },\n }),\n }),\n});",
"$type": "app.offprint.block.codeBlock",
"language": "ts"
},
{
"$type": "app.offprint.block.text",
"facets": [
{
"index": {
"byteEnd": 203,
"byteStart": 181
},
"features": [
{
"$type": "app.offprint.richtext.facet#code"
}
]
}
],
"plaintext": "You'll note that the lexicon is pulled straight from the package, and we didn't have to install it. Also, there's a \"renderer\" option. In this case, I've defined a renderer to turn site.standard.document records into HTML. You could define a renderer for any kind of record if desired - I felt this one was important enough to publish with the package."
},
{
"$type": "app.offprint.block.text",
"plaintext": "And, the post you're reading right now was published on Offprint. Looks pretty good, right?"
},
{
"$type": "app.offprint.block.horizontalRule"
},
{
"$type": "app.offprint.block.text",
"plaintext": "Let me know if you give my package a shot! I hope I could help to make your Astro site a bit more Atmospheric."
}
]
},
"description": "How I use content from AT Protocol on my Astro static site.",
"publishedAt": "2026-05-04T00:07:31+00:00",
"textContent": "I've been slowly making my website more and more Atmospheric. Services like offprint.app, pckt.blog, leaflet.pub, and others have all began to use the standard.site lexicon to give a consistent lexicon for referencing these documents.\nBut each of the apps supports different content blocks and different rendering tasks. There is no shared schema for rendering of documents. The central lexicon is primarily for making posts referenceable across the Atmosphere.\nI really wanted to use one of these services (or maybe several of these services) as a CMS of sorts. Where I could post on Offprint and have it be mirrored on my website.\nSo, I built a small package called at-astro-loader. We can just build things, I guess!\nFirst Schema\nOn my website, I have a projects section, which looks like this:\nTo make my website more Atmospheric, I wanted these projects to be referenced from my PDS. So, I defined a custom lexicon, com.chrisvanderloo.project, with a simple schema that you can find here on PDSLS. There's a great guide on atproto.com that helps with the basics of publishing your first schema.\nAfter publishing that, I used lex to install my new lexicon. I used npx -p @atproto/lex lex install com.chrisvanderloo.project . Then, I generated a src/lexicons folder using lex build . Finally, I had a type-safe schema to reference.\nIn Astro, you define a content.config.ts file to create content collections. I wanted my projects to be loaded as a content collection so I could reference it at build. Using my at-astro-loader package, it's pretty easy to set up a content collection based on an installed schema. I'm having it pull straight from my PDS, and repo references my username.\nimport { defineCollection } from \"astro:content\";\nimport * as com from \"./lexicons/com\";\nimport { atLoader } from \"at-astro-loader\";\n\nconst projects = defineCollection({\n loader: atLoader(com.chrisvanderloo.project, {\n endpoint: \"https://bsky.chrisvanderloo.com\",\n repo: \"chrisvanderloo.com\",\n }),\n});\n\n\nexport const collections = {\n projects,\n};\n The above configuration pulls my projects at build time, easy peasy.\nOffprint Posts\nBut now, how do I do this with Offprint posts? I have articles I've written in Markdown, and those are turned into pages at build time. I'd like it to pull Offprint posts as well.\nWith 1.2.1 of at-astro-loader, I added support for Offprint documents. The loader is used as follows:\nimport { defineCollection } from \"astro:content\";\nimport { atLoader, defineStandardSiteDocumentRenderer } from \"at-astro-loader\";\nimport { site } from \"at-astro-loader/lexicons\";\n\nconst atprotoBlogCollection = defineCollection({\n loader: atLoader(site.standard.document, {\n endpoint: \"https://bsky.chrisvanderloo.com\",\n repo: \"chrisvanderloo.com\",\n renderer: defineStandardSiteDocumentRenderer({\n shikiConfig: {\n themes: {\n light: \"catppuccin-latte\",\n dark: \"catppuccin-mocha\",\n },\n },\n }),\n }),\n});\nYou'll note that the lexicon is pulled straight from the package, and we didn't have to install it. Also, there's a \"renderer\" option. In this case, I've defined a renderer to turn site.standard.document records into HTML. You could define a renderer for any kind of record if desired - I felt this one was important enough to publish with the package.\nAnd, the post you're reading right now was published on Offprint. Looks pretty good, right?\n\n---\nLet me know if you give my package a shot! I hope I could help to make your Astro site a bit more Atmospheric."
}