···11+# at-publisher
22+33+To install dependencies:
44+55+```bash
66+bun install
77+```
88+99+To run:
1010+1111+```bash
1212+bun run index.ts
1313+```
1414+1515+This project was created using `bun init` in bun v1.3.5. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
···11+# CLI Reference
22+33+## `auth`
44+55+```bash [Terminal]
66+sequoia auth
77+> Authenticate with your ATProto PDS
88+99+OPTIONS:
1010+ --logout <str> - Remove credentials for a specific identity (or all if only one exists) [optional]
1111+1212+FLAGS:
1313+ --list - List all stored identities [optional]
1414+ --help, -h - show help [optional]
1515+```
1616+1717+## `init`
1818+1919+```bash [Terminal]
2020+sequoia init
2121+> Initialize a new publisher configuration
2222+2323+FLAGS:
2424+ --help, -h - show help [optional]
2525+```
2626+2727+## `publish`
2828+2929+```bash [Terminal]
3030+sequoia publish
3131+> Publish content to ATProto
3232+3333+FLAGS:
3434+ --force, -f - Force publish all posts, ignoring change detection [optional]
3535+ --dry-run, -n - Preview what would be published without making changes [optional]
3636+ --help, -h - show help [optional]
3737+```
3838+3939+## `inject`
4040+4141+```bash [Terminal]
4242+sequoia inject
4343+> Inject site.standard.document link tags into built HTML files
4444+4545+OPTIONS:
4646+ --output, -o <str> - Output directory to scan for HTML files [optional]
4747+4848+FLAGS:
4949+ --dry-run, -n - Preview what would be injected without making changes [optional]
5050+ --help, -h - show help [optional]
5151+```
5252+5353+## `sync`
5454+5555+```bash [Terminal]
5656+sequoia sync
5757+> Sync state from ATProto to restore .sequoia-state.json
5858+5959+FLAGS:
6060+ --update-frontmatter, -u - Update frontmatter atUri fields in local markdown files [optional]
6161+ --dry-run, -n - Preview what would be synced without making changes [optional]
6262+ --help, -h - show help [optional]
6363+```
+62
docs/docs/pages/config.mdx
···11+# Configuration Reference
22+33+## `sequoia.json`
44+55+| Field | Type | Required | Default | Description |
66+|-------|------|----------|---------|-------------|
77+| `siteUrl` | `string` | Yes | - | Base URL of your website |
88+| `contentDir` | `string` | Yes | - | Directory containing blog post files |
99+| `publicationUri` | `string` | Yes | - | AT-URI of your publication record |
1010+| `imagesDir` | `string` | No | - | Directory containing cover images |
1111+| `publicDir` | `string` | No | `"./public"` | Static folder for `.well-known` files |
1212+| `outputDir` | `string` | No | - | Built output directory for inject command |
1313+| `pathPrefix` | `string` | No | `"/posts"` | URL path prefix for posts |
1414+| `pdsUrl` | `string` | No | `"https://bsky.social"` | PDS server URL, generated automatically |
1515+| `identity` | `string` | No | - | Which stored identity to use |
1616+| `frontmatter` | `object` | No | - | Custom frontmatter field mappings |
1717+1818+### Example
1919+2020+```json
2121+{
2222+ "siteUrl": "https://myblog.com",
2323+ "contentDir": "./content/posts",
2424+ "publicationUri": "at://did:plc:abc123/site.standard.publication/self",
2525+ "pathPrefix": "/blog"
2626+}
2727+```
2828+2929+## Post Frontmatter
3030+3131+| Field | Type | Required | Default Mapping | Description |
3232+|-------|------|----------|-----------------|-------------|
3333+| `title` | `string` | Yes | `"title"` | Post title |
3434+| `description` | `string` | No | `"description"` | Post description/summary |
3535+| `publishDate` | `string` | Yes | `"publishDate"`, `"pubDate"`, `"date"`, `"createdAt"`, `"created_at"` | Publication date |
3636+| `coverImage` | `string` | No | `"ogImage"` | Cover image filename |
3737+| `tags` | `string[]` | No | `"tags"` | Post tags/categories |
3838+3939+### Example
4040+4141+```yaml
4242+---
4343+title: My First Post
4444+description: An introduction to my blog
4545+publishDate: 2024-01-15
4646+ogImage: cover.jpg
4747+tags: [welcome, intro]
4848+---
4949+```
5050+5151+### Custom Frontmatter Mapping
5252+5353+Override default field names in `sequoia.json`:
5454+5555+```json
5656+{
5757+ "frontmatter": {
5858+ "publishDate": "date",
5959+ "coverImage": "thumbnail"
6060+ }
6161+}
6262+```
+16
docs/docs/pages/index.mdx
···11+---
22+layout: landing
33+---
44+55+import { HomePage } from 'vocs/components'
66+77+<HomePage.Root>
88+ <HomePage.Logo />
99+ <HomePage.Tagline>Publish evergreen content to the ATmosphere</HomePage.Tagline>
1010+ <HomePage.InstallPackage name="-g sequoia-cli" type="install" />
1111+ <HomePage.Description>A simple CLI for creating standard.site documents from your existing static blog.</HomePage.Description>
1212+ <HomePage.Buttons>
1313+ <HomePage.Button href="/quickstart" variant="accent">Get started</HomePage.Button>
1414+ <HomePage.Button href="">Tangled</HomePage.Button>
1515+ </HomePage.Buttons>
1616+</HomePage.Root>
+29
docs/docs/pages/publishing.mdx
···11+# Publishing
22+33+Sequoia makes it simple to publish standard.site documents to the AT Protocol.
44+55+## Basics
66+77+After completing the initial [setup](/setup) for Sequoia, it's time to publish. Before you go live, try using the `--dry-run` flag to see what will be published.
88+99+```bash [Terminal]
1010+sequoia publish --dry-run
1111+```
1212+1313+This will print out the posts that it has discovered, what will be published, and how many. Once everything looks good, send it!
1414+1515+```bash [Terminal]
1616+sequoia publish
1717+```
1818+1919+## Updates
2020+2121+As you publish content Sequoia will store a record of content in `.sequoia-state.json`, keeping tack of the AT URIs for each post and generating content hashes for the content inside each markdown file. When you update a post and run the publish command, Sequoia will generate hashes again, see the difference, and update the existing post rather than creating a new one.
2222+2323+If you happen to loose the state file or if you want to pull down records you already have published, you can use the `sync` command.
2424+2525+```bash [Terminal]
2626+seuqoia sync
2727+```
2828+2929+Sync will use your ATProto handle to look through all of the `standard.site.document` records on your PDS, and pull down the records that are for the publication in the config.
+106
docs/docs/pages/quickstart.mdx
···11+# Quickstart
22+33+If you have an existing blog using a static site generator (SSG) and you want to publish [Standard.site](https://standard.site) documents to the AT Protocol, you're in the right place!
44+55+Sequoia is a simple CLI tool that will help get your initial publication setup, as well as publish documents as you deploy your site. Just follow these steps to get started.
66+77+88+::::steps
99+1010+### Install Sequoia
1111+1212+Install with your package manager of choice
1313+1414+:::code-group
1515+```bash [npm]
1616+npm i -g sequoia-cli
1717+```
1818+1919+```bash [pnpm]
2020+pnpm i -g sequoia-cli
2121+```
2222+2323+```bash [bun]
2424+bun i -g sequoia-cli
2525+```
2626+:::
2727+2828+Check to make sure it was installed correctly by running the `sequoia` command
2929+3030+```bash [Terminal]
3131+sequoia
3232+```
3333+3434+### Authorize
3535+3636+In order for Sequoia to publish or update records on your PDS, you need to authoize it with your ATProto handle and an app password.
3737+3838+:::tip
3939+You can create an app password [here](https://bsky.app/settings/app-passwords)
4040+:::
4141+4242+```bash [Terminal]
4343+sequoia auth
4444+```
4545+4646+### Initialize
4747+4848+After you have authorized Sequoia, the next step is to setup your blog as a publication. First make sure you are in your main repo for your blog SSG, then run the following:
4949+5050+```bash [Terminal]
5151+sequoia init
5252+```
5353+5454+At this point you will be asked a series of questions to help setup Sequoia for your blog. Here is a brief overview and description for each step:
5555+5656+- **Site URL** - The primary URL for your blog, e.g. `https://sequoia.pub`
5757+- **Content directory** - This is where your markdown files live when creating blog posts, e.g. `./src/content/blog`
5858+- **Cover images directory** - When creating standard.site documents, there is an optional `coverImage` that is used like an Opengraph image, a preview of what is in your blog. With this setting you can give a specific path to your image folder where these live, e.g. `./src/assets`. If you don't use one, then you can just press enter to leave it blank.
5959+- **Public/static directory** - The path for the folder where your public items go, e.g. `./public`. Generally used for opengraph images or icons, but in this case we need it to store a `.well-known` verification for your blog, [read more here](/verifying).
6060+- **Build output directory** - Where you published html css and js lives, e.g. `./dist`
6161+- **URL path prefix for posts** - The path that goes before a post slug, e.g. the prefix for `https://sequoia.pub/blog/hello` would be `/blog`.
6262+- **Configure your frontmatter field mappings** - In your markdown posts there is usually frontmatter with infomation like `title`, `description`, and `publishedDate`. Follow the prompts and enter the names for your frontmatter fields so Sequoia can use them for creating standard.site documents.
6363+- **Publication setup** - Here you can choose to `Create a new publication` which will create a `site.standard.publication` record on your PDS, or you can `Use an existing publication AT URI`. If you haven't done this before, select `Create a new publication`.
6464+ - **Publication name** - The name of your blog
6565+ - **Publication description** - A description for your blog
6666+ - **Icon image path** - An optional path to your blog icon image, e.g. `./public/icon.png`. This can be left blank if you choose not to use it.
6767+ - **Show in Discover feed?** - A yes or no to mark your publication whether or not you want it to be discovered by aggregators.
6868+6969+Once you complete the initialization step the following will happen:
7070+- A new `site.standard.publication` record will be created on your PDS (if you chose to in the setup)
7171+- A new `sequoia.json` config file will be created in your project repo
7272+- A `.well-known/site.standard.publication` record will be saved to your public/static folder
7373+7474+### Publish
7575+7676+You are now ready to publish your blog to the ATmosphere! Just run the following command to make sure the configuration has been setup correctly.
7777+7878+```bash [Terminal]
7979+sequoia publish --dry-run
8080+```
8181+8282+This will print out the posts that it has discovered, what will be published, and how many. Once everything looks good, send it!
8383+8484+```bash [Terminal]
8585+sequoia publish
8686+```
8787+8888+### Verify
8989+9090+The standard.site records are now published on your PDS, however there is one final step in order for aggregators to find your content: verification. There are two main pieces:
9191+9292+#### Publication Verification
9393+9494+If you remember, Sequoia asked for your public/static directory. Once the publication was created, it stored a `.well-known/site.standard.publication` file there. This way once you build and deploy your site using your SSG, the file can be accessed with `https://sequoia.pub/.well-known/site.standard.publication` by aggregators. So you're already half way there; just build and deploy!
9595+9696+#### Document Verification
9797+9898+Every document or blog post that is published needs a `<link>` tag in the `<head>` of your blog post HTML page. The content of that link tag needs to be the AT URI for the record we just published on your PDS. There are two ways you can handle these:
9999+- `sequoia inject` (recommended) - By running this command after publishing, and after building the site with your SSG, Sequoia will inject the link tags into your finished HTML. This way you don't have to manually edit it or mess with an SSG config to set it up. Just deploy the build folder after you have run `sequoia inject`!
100100+- Manual - After you have run `sequoia publish` the CLI will add in a new `atUri` field to every post's frontmatter. This way you can configure your SSG to read that frontmatter and include it in the build step, similar to how it might include an opengraph image in the meta tags. This approach gives you full control over the HTML files but will take a bit more skill.
101101+102102+::::
103103+104104+**Congratulations! 🎉**
105105+106106+You just published your blog to the AT Protocol with standard.site lexicons! There is certainly much more to learn about ATProto or Sequoia, but at least you got your foot in the door. Keep reading to find out what else is possible!
+69
docs/docs/pages/setup.mdx
···11+# Setup
22+33+This guide covers the various aspects for setting up Sequoia
44+55+## Install
66+77+Install with your package manager of choice
88+99+:::code-group
1010+```bash [npm]
1111+npm i -g sequoia-cli
1212+```
1313+1414+```bash [pnpm]
1515+pnpm i -g sequoia-cli
1616+```
1717+1818+```bash [bun]
1919+bun i -g sequoia-cli
2020+```
2121+:::
2222+2323+Check to make sure it was installed correctly by running the `sequoia` command
2424+2525+```bash [Terminal]
2626+sequoia
2727+```
2828+2929+## Authorize
3030+3131+In order for Sequoia to publish or update records on your PDS, you need to authoize it with your ATProto handle and an app password.
3232+3333+:::tip
3434+You can create an app password [here](https://bsky.app/settings/app-passwords)
3535+:::
3636+3737+```bash [Terminal]
3838+sequoia auth
3939+```
4040+4141+This will store credentials as a dotfile directory inside `$HOME/.config/sequoia`. If you happen to have more than one blog or publication, you can authorize additional accounts. During the initialize phase the CLI will ask which account you want to use.
4242+4343+## Initialize
4444+4545+After you have authorized Sequoia, the next step is to setup your blog as a publication. First make sure you are in your main repo for your blog SSG, then run the following:
4646+4747+```bash [Terminal]
4848+sequoia init
4949+```
5050+5151+At this point you will be asked a series of questions to help setup Sequoia for your blog. Here is a brief overview and description for each step:
5252+5353+- **Site URL** - The primary URL for your blog, e.g. `https://sequoia.pub`
5454+- **Content directory** - This is where your markdown files live when creating blog posts, e.g. `./src/content/blog`
5555+- **Cover images directory** - When creating standard.site documents, there is an optional `coverImage` that is used like an Opengraph image, a preview of what is in your blog. With this setting you can give a specific path to your image folder where these live, e.g. `./src/assets`. If you don't use one, then you can just press enter to leave it blank.
5656+- **Public/static directory** - The path for the folder where your public items go, e.g. `./public`. Generally used for opengraph images or icons, but in this case we need it to store a `.well-known` verification for your blog, [read more here](/verifying).
5757+- **Build output directory** - Where you published html css and js lives, e.g. `./dist`
5858+- **URL path prefix for posts** - The path that goes before a post slug, e.g. the prefix for `https://sequoia.pub/blog/hello` would be `/blog`.
5959+- **Configure your frontmatter field mappings** - In your markdown posts there is usually frontmatter with infomation like `title`, `description`, and `publishedDate`. Follow the prompts and enter the names for your frontmatter fields so Sequoia can use them for creating standard.site documents.
6060+- **Publication setup** - Here you can choose to `Create a new publication` which will create a `site.standard.publication` record on your PDS, or you can `Use an existing publication AT URI`. If you haven't done this before, select `Create a new publication`.
6161+ - **Publication name** - The name of your blog
6262+ - **Publication description** - A description for your blog
6363+ - **Icon image path** - An optional path to your blog icon image, e.g. `./public/icon.png`. This can be left blank if you choose not to use it.
6464+ - **Show in Discover feed?** - A yes or no to mark your publication whether or not you want it to be discovered by aggregators.
6565+6666+Once you complete the initialization step the following will happen:
6767+- A new `site.standard.publication` record will be created on your PDS (if you chose to in the setup)
6868+- A new `sequoia.json` config file will be created in your project repo
6969+- A `.well-known/site.standard.publication` record will be saved to your public/static folder
+26
docs/docs/pages/supported-frameworks.mdx
···11+# Supported Frameworks
22+33+:::note
44+This list is actively being updated as more static site generators and frameworks are being tested and integrated. If you see something missing, [make a request]()!
55+:::
66+77+| Framework | Status |
88+|------|--------|
99+| Astro | ✅ |
1010+| Hugo | ? |
1111+| 11ty | ? |
1212+| Next.js | ? |
1313+| Gatsby | ? |
1414+| Nuxt | ? |
1515+| SvelteKit | ? |
1616+| Remix | ? |
1717+| Jekyll | ? |
1818+| Docusaurus | ? |
1919+| VuePress | ? |
2020+| Gridsome | ? |
2121+| Scully | ? |
2222+| Elder.js | ? |
2323+| Bridgetown | ? |
2424+| Zola | ? |
2525+| Pelican | ? |
2626+| Hexo | ? |
+17
docs/docs/pages/verifying.mdx
···11+# Verifying
22+33+In order for your posts to show up on indexers, the chances are you need to make sure your publication and your documents are verified.
44+55+:::tip
66+You an learn more about Standard.site verification [here](https://standard.site/)
77+:::
88+99+## Publication Verification
1010+1111+As specified by Standard.site, the `site.standard.publication` record is verified by placing the record `https://example.com/.well-known/site.standard.publication`. That record might look something like `at://did:plc:abc123/site.standard.publication/rkey`. Sequoia handles this for you automatically if you designate your public/static folder during the [setup](/setup). When the record is created, the record AT URI is saved in `.well-known/site.standard.publication` of your public folder. Once you deploy your site with this addition, the publication will be verified!
1212+1313+## Document Verification
1414+1515+Every document or blog post that is published needs a `<link>` tag in the `<head>` of your blog post HTML page. The content of that link tag needs to be the AT URI for the record we just published on your PDS. There are two ways you can handle these:
1616+- `sequoia inject` (recommended) - By running this command after publishing, and after building the site with your SSG, Sequoia will inject the link tags into your finished HTML. This way you don't have to manually edit it or mess with an SSG config to set it up. Just deploy the build folder after you have run `sequoia inject`!
1717+- Manual - After you have run `sequoia publish` the CLI will add in a new `atUri` field to every post's frontmatter. This way you can configure your SSG to read that frontmatter and include it in the build step, similar to how it might include an opengraph image in the meta tags. This approach gives you full control over the HTML files but will take a bit more skill.
+11
docs/docs/pages/what-is-sequoia.mdx
···11+# What is Sequoia?
22+33+Sequoia is a simple CLI that can be used to publish Standard.site lexicons to the AT Protocol. Yeah that's a mouthful; let's break it down.
44+55+- [AT Protocol](https://atproto.com) - As the site says, "The AT Protocol is an open, decentralized network for building social applications." In reality it's a bit more than that. It's a new way to publish content to the web that puts control back in the hands of users without sacrificing distrubtion. There's a lot to unpack, but you can find a primer [here](https://stevedylan.dev/posts/atproto-starter/).
66+- [Lexicons](https://atproto.com/guides/lexicon) - Lexicons are schemas used inside the AT Protocol. If you were to "like" a post, what would that consist of? Probably _who_ liked it, _what_ post was liked, and the _author_ of the post. The unique property to lexicons is that anyone can publish them and have them verified under a domain. Then these lexicons can be used to build apps by pulling a users records, aggregating them using an indexer, and a whole lot more!
77+- [Standard.site](https://standard.site) - Standard.site is a set of lexicons specailly designed for publishing content. It was started by the founders of [leaflet.pub](https://leaflet.pub), [pckt.blog](https://pckt.blog), and [offprint.app](https://offprint.app), with the mission of finding a schema that can be used for blog posts and blog sites themselves. So far it has proven to be the lexicon of choice for publishing content to ATProto with multiple tools and lexicons revolving around the standard.
88+99+The goal of Sequoia is to make it easier for those with existing self-hosted blogs to publish their content to the ATmosphere, no matter what SSG or framework you might be using. As of right now the focus will be static sites, but if there is enough traction there might be a future package that can be used for SSR frameworks too.
1010+1111+Sequoia is MIT open sourced and available on [Tangled](https://tangled.org/stevedylan.dev) and [GitHub](https://github.com/stevedylandev/sequoia).
+5
docs/docs/pages/workflows.mdx
···11+# Workflows
22+33+:::warning
44+Under construction
55+:::
docs/docs/public/icon.png
This is a binary file and will not be displayed.
docs/docs/public/og.png
This is a binary file and will not be displayed.
+88
docs/inject-og-tags.ts
···11+#!/usr/bin/env bun
22+33+import { readdirSync, statSync } from "node:fs";
44+import { join } from "node:path";
55+66+const distDir = "./docs/dist";
77+const ogImageUrl = "https://sequoia.pub/og.png";
88+99+// Function to recursively find all HTML files
1010+function findHtmlFiles(dir: string): string[] {
1111+ const files: string[] = [];
1212+ const entries = readdirSync(dir);
1313+1414+ for (const entry of entries) {
1515+ const fullPath = join(dir, entry);
1616+ const stat = statSync(fullPath);
1717+1818+ if (stat.isDirectory()) {
1919+ files.push(...findHtmlFiles(fullPath));
2020+ } else if (entry.endsWith(".html")) {
2121+ files.push(fullPath);
2222+ }
2323+ }
2424+2525+ return files;
2626+}
2727+2828+// Function to inject OG image meta tags
2929+async function injectOgImageTags(filePath: string) {
3030+ const file = Bun.file(filePath);
3131+ let content = await file.text();
3232+3333+ // Check if og:image already exists
3434+ if (content.includes('property="og:image"')) {
3535+ console.log(`⏭️ Skipping ${filePath} - og:image already exists`);
3636+ return;
3737+ }
3838+3939+ // Find the position to inject the meta tag
4040+ // We'll insert it after og:description if it exists, or before twitter:card
4141+ const ogDescriptionMatch = content.match(
4242+ /<meta property="og:description"[^>]*>/,
4343+ );
4444+ const twitterCardMatch = content.match(/<meta name="twitter:card"[^>]*>/);
4545+4646+ let insertPosition: number;
4747+ if (ogDescriptionMatch && ogDescriptionMatch.index !== undefined) {
4848+ insertPosition = ogDescriptionMatch.index + ogDescriptionMatch[0].length;
4949+ } else if (twitterCardMatch && twitterCardMatch.index !== undefined) {
5050+ insertPosition = twitterCardMatch.index;
5151+ } else {
5252+ // Fallback: insert before </head>
5353+ const headCloseMatch = content.indexOf("</head>");
5454+ if (headCloseMatch === -1) {
5555+ console.log(`⚠️ Warning: Could not find insertion point in ${filePath}`);
5656+ return;
5757+ }
5858+ insertPosition = headCloseMatch;
5959+ }
6060+6161+ // Inject the og:image and twitter:image meta tags
6262+ const ogImageTag = `<meta property="og:image" content="${ogImageUrl}"/>`;
6363+ const twitterImageTag = `<meta name="twitter:image" content="${ogImageUrl}"/>`;
6464+ const newContent =
6565+ content.slice(0, insertPosition) +
6666+ ogImageTag +
6767+ twitterImageTag +
6868+ content.slice(insertPosition);
6969+7070+ // Write the modified content back to the file
7171+ await Bun.write(filePath, newContent);
7272+ console.log(`✅ Injected og:image tags into ${filePath}`);
7373+}
7474+7575+// Main execution
7676+async function main() {
7777+ console.log("🔍 Finding HTML files in dist directory...");
7878+ const htmlFiles = findHtmlFiles(distDir);
7979+ console.log(`📄 Found ${htmlFiles.length} HTML files`);
8080+8181+ for (const file of htmlFiles) {
8282+ await injectOgImageTags(file);
8383+ }
8484+8585+ console.log("\n✨ Done! All HTML files have been processed.");
8686+}
8787+8888+main();