jekyll-standard-site 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +100 -20
- data/lib/jekyll-standard-site/publisher.rb +123 -0
- data/lib/jekyll-standard-site/tasks.rb +30 -0
- data/lib/jekyll-standard-site/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 26ecdb29d4af51a0e59c0edc928d346ab5b999800755e2d461efb442dd77ba69
|
|
4
|
+
data.tar.gz: 2c1bda0b284aa9e3293cdb8acf1faf3b41bf9dd7699f1d092af0695a3a484462
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b0b1cccd4f5f28ab56c90957330cb9c3f8eb9de5707e6c54f001769c0793770ea9a604c770f5ac9b3074ffa5d1097f54dbff90704a7654d9d9a9c3e92c264147
|
|
7
|
+
data.tar.gz: ebc0a6af25b249a9e694862566ea940c6ef4c9c5b95f755a096a1683268560ccdcb00484aaf579a906c78c2f1cd7f5e3d0126f794accb9d3d7ccd830f8f47732
|
data/README.md
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
A Jekyll plugin that emits the verification artifacts required by [standard.site](https://standard.site), the AT Protocol lexicons for long-form publishing.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It does two things:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
1. **At build time**, emits the `.well-known` endpoint and the `<link>` tags that prove your site owns its publication and document records.
|
|
8
|
+
2. **At publish time**, optionally creates `site.standard.document` records on your PDS and writes the returned AT-URIs back into each post's front matter, so the next build emits the document verification link.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
## Installation
|
|
10
11
|
|
|
11
12
|
```ruby
|
|
12
13
|
group :jekyll_plugins do
|
|
@@ -14,7 +15,7 @@ group :jekyll_plugins do
|
|
|
14
15
|
end
|
|
15
16
|
```
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
In `_config.yml`:
|
|
18
19
|
|
|
19
20
|
```yaml
|
|
20
21
|
plugins:
|
|
@@ -24,13 +25,24 @@ standard_site:
|
|
|
24
25
|
publication: "at://did:plc:abc123/site.standard.publication/3lwafzkjqm25s"
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
##
|
|
28
|
+
## Bootstrap: create the publication record
|
|
29
|
+
|
|
30
|
+
This gem does **not** create the publication record. That's a one-time step you do out-of-band, then paste the resulting AT-URI into `_config.yml`.
|
|
31
|
+
|
|
32
|
+
Pick whichever flow you prefer:
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
- The [standard.site](https://standard.site) dashboard
|
|
35
|
+
- [`goat`](https://github.com/bluesky-social/indigo/tree/main/cmd/goat) — `goat record create --collection site.standard.publication --record '{"url":"https://example.com","name":"Example"}'`
|
|
36
|
+
- [`sequoia-cli`](https://sequoia.pub)
|
|
37
|
+
- A direct `com.atproto.repo.createRecord` call
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
Once the record exists, set `standard_site.publication` and the well-known endpoint plus the publication discovery hint start working immediately on the next build.
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
## Build-time behaviour
|
|
42
|
+
|
|
43
|
+
### `.well-known/site.standard.publication`
|
|
44
|
+
|
|
45
|
+
Written automatically from `standard_site.publication`. If your publication is not at the domain root, set `publication_path`:
|
|
34
46
|
|
|
35
47
|
```yaml
|
|
36
48
|
standard_site:
|
|
@@ -38,26 +50,94 @@ standard_site:
|
|
|
38
50
|
publication_path: "/blog"
|
|
39
51
|
```
|
|
40
52
|
|
|
41
|
-
This writes to `/.well-known/site.standard.publication/blog
|
|
53
|
+
This writes to `/.well-known/site.standard.publication/blog`.
|
|
42
54
|
|
|
43
|
-
###
|
|
55
|
+
### Verification link tags
|
|
44
56
|
|
|
45
|
-
|
|
57
|
+
Drop the Liquid tag into your `<head>` layout:
|
|
46
58
|
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
title: My First Post
|
|
50
|
-
at_uri: "at://did:plc:abc123/site.standard.document/3mek5jhkri72r"
|
|
51
|
-
---
|
|
59
|
+
```liquid
|
|
60
|
+
{% standard_site_links %}
|
|
52
61
|
```
|
|
53
62
|
|
|
54
|
-
|
|
63
|
+
It emits a `site.standard.publication` discovery hint on every page, and a `site.standard.document` link tag on any page whose front matter includes `at_uri`.
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
## Publishing documents
|
|
66
|
+
|
|
67
|
+
To verify individual posts and have them discoverable via standard.site readers, each post needs a `site.standard.document` record on your PDS, plus its AT-URI in the post's front matter as `at_uri`.
|
|
68
|
+
|
|
69
|
+
This gem ships a Rake task that handles both. Add to your `Rakefile`:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
require "jekyll-standard-site/tasks"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Then:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
BSKY_HANDLE=you.bsky.social BSKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx bundle exec rake standard_site:publish
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The task:
|
|
82
|
+
|
|
83
|
+
1. Logs in via `com.atproto.server.createSession` using the credentials in env vars.
|
|
84
|
+
2. Reads `_posts` and finds posts whose front matter has no `at_uri`.
|
|
85
|
+
3. For each, calls `com.atproto.repo.createRecord` with a `site.standard.document` payload built from the post's `title`, `description`, `tags`, `date`, and URL.
|
|
86
|
+
4. Patches the returned AT-URI back into the post file as `at_uri: "..."`.
|
|
87
|
+
|
|
88
|
+
It's idempotent. Re-running skips any post that already has `at_uri`.
|
|
89
|
+
|
|
90
|
+
Env vars:
|
|
91
|
+
|
|
92
|
+
- `BSKY_HANDLE` — your handle (e.g. `you.bsky.social`).
|
|
93
|
+
- `BSKY_APP_PASSWORD` — generate at <https://bsky.app/settings/app-passwords>. Use a dedicated one.
|
|
94
|
+
- `BSKY_PDS` — defaults to `https://bsky.social`. Override if you self-host.
|
|
95
|
+
|
|
96
|
+
### GitHub Actions
|
|
97
|
+
|
|
98
|
+
A workflow that publishes documents whenever new posts land on `master`:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
name: Publish documents
|
|
102
|
+
|
|
103
|
+
on:
|
|
104
|
+
push:
|
|
105
|
+
branches: ["master"]
|
|
106
|
+
paths: ["_posts/**"]
|
|
107
|
+
|
|
108
|
+
permissions: {}
|
|
109
|
+
|
|
110
|
+
concurrency:
|
|
111
|
+
group: standard-site-publish
|
|
112
|
+
cancel-in-progress: false
|
|
113
|
+
|
|
114
|
+
jobs:
|
|
115
|
+
publish:
|
|
116
|
+
if: github.actor != 'github-actions[bot]'
|
|
117
|
+
runs-on: ubuntu-latest
|
|
118
|
+
permissions:
|
|
119
|
+
contents: write
|
|
120
|
+
steps:
|
|
121
|
+
- uses: actions/checkout@v6
|
|
122
|
+
- uses: ruby/setup-ruby@v1
|
|
123
|
+
with:
|
|
124
|
+
bundler-cache: true
|
|
125
|
+
- run: bundle exec rake standard_site:publish
|
|
126
|
+
env:
|
|
127
|
+
BSKY_HANDLE: ${{ secrets.BSKY_HANDLE }}
|
|
128
|
+
BSKY_APP_PASSWORD: ${{ secrets.BSKY_APP_PASSWORD }}
|
|
129
|
+
- name: Commit patched front matter
|
|
130
|
+
run: |
|
|
131
|
+
if [[ -n "$(git status --porcelain _posts)" ]]; then
|
|
132
|
+
git config user.name "github-actions[bot]"
|
|
133
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
134
|
+
git add _posts
|
|
135
|
+
git commit -m "Add at_uri to new posts [skip ci]"
|
|
136
|
+
git push
|
|
137
|
+
fi
|
|
58
138
|
```
|
|
59
139
|
|
|
60
|
-
|
|
140
|
+
The `[skip ci]` plus the `github.actor` guard stop the bot's own commits from re-triggering the workflow.
|
|
61
141
|
|
|
62
142
|
## License
|
|
63
143
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "json"
|
|
3
|
+
require "uri"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Jekyll
|
|
7
|
+
module StandardSite
|
|
8
|
+
class PublisherError < StandardError; end
|
|
9
|
+
|
|
10
|
+
class HttpClient
|
|
11
|
+
def post_json(url, body, headers = {})
|
|
12
|
+
uri = URI(url)
|
|
13
|
+
req = Net::HTTP::Post.new(uri)
|
|
14
|
+
req["Content-Type"] = "application/json"
|
|
15
|
+
headers.each { |k, v| req[k] = v }
|
|
16
|
+
req.body = JSON.dump(body)
|
|
17
|
+
|
|
18
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
19
|
+
http.request(req)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
parsed = res.body && !res.body.empty? ? JSON.parse(res.body) : {}
|
|
23
|
+
[res.code.to_i, parsed]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Publisher
|
|
28
|
+
DEFAULT_PDS = "https://bsky.social".freeze
|
|
29
|
+
|
|
30
|
+
def initialize(site:, handle:, password:, pds: DEFAULT_PDS, http: HttpClient.new, logger: Jekyll.logger)
|
|
31
|
+
@site = site
|
|
32
|
+
@handle = handle
|
|
33
|
+
@password = password
|
|
34
|
+
@pds = pds.sub(%r{/\z}, "")
|
|
35
|
+
@http = http
|
|
36
|
+
@logger = logger
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def publish_all
|
|
40
|
+
publication = StandardSite.publication_uri(@site)
|
|
41
|
+
raise PublisherError, "standard_site.publication is not set in _config.yml" unless publication
|
|
42
|
+
|
|
43
|
+
session = login
|
|
44
|
+
published = []
|
|
45
|
+
|
|
46
|
+
unpublished_posts.each do |post|
|
|
47
|
+
uri = create_document_record(session, publication, post)
|
|
48
|
+
patch_front_matter(post.path, uri)
|
|
49
|
+
@logger.info "Standard.site:", "published #{post.relative_path} -> #{uri}"
|
|
50
|
+
published << [post.relative_path, uri]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
published
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def unpublished_posts
|
|
57
|
+
@site.read if @site.posts.docs.empty?
|
|
58
|
+
@site.posts.docs.reject { |p| p.data["at_uri"] }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def login
|
|
62
|
+
code, body = @http.post_json(
|
|
63
|
+
"#{@pds}/xrpc/com.atproto.server.createSession",
|
|
64
|
+
{ "identifier" => @handle, "password" => @password }
|
|
65
|
+
)
|
|
66
|
+
raise PublisherError, "login failed (#{code}): #{body["message"] || body}" unless code == 200
|
|
67
|
+
body
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def create_document_record(session, publication, post)
|
|
71
|
+
record = build_record(publication, post)
|
|
72
|
+
code, body = @http.post_json(
|
|
73
|
+
"#{@pds}/xrpc/com.atproto.repo.createRecord",
|
|
74
|
+
{
|
|
75
|
+
"repo" => session["did"],
|
|
76
|
+
"collection" => "site.standard.document",
|
|
77
|
+
"record" => record
|
|
78
|
+
},
|
|
79
|
+
"Authorization" => "Bearer #{session["accessJwt"]}"
|
|
80
|
+
)
|
|
81
|
+
raise PublisherError, "createRecord failed for #{post.relative_path} (#{code}): #{body["message"] || body}" unless code == 200
|
|
82
|
+
body["uri"]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def build_record(publication, post)
|
|
86
|
+
data = post.data
|
|
87
|
+
record = {
|
|
88
|
+
"$type" => "site.standard.document",
|
|
89
|
+
"site" => publication,
|
|
90
|
+
"title" => data["title"].to_s,
|
|
91
|
+
"publishedAt" => normalize_time(post.date)
|
|
92
|
+
}
|
|
93
|
+
record["path"] = post.url if post.url
|
|
94
|
+
record["description"] = data["description"] if data["description"]
|
|
95
|
+
record["tags"] = Array(data["tags"]) if data["tags"] && !Array(data["tags"]).empty?
|
|
96
|
+
record["updatedAt"] = normalize_time(data["updated"]) if data["updated"]
|
|
97
|
+
record
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def normalize_time(value)
|
|
101
|
+
case value
|
|
102
|
+
when Time then value.utc.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
|
103
|
+
when String then value
|
|
104
|
+
else raise PublisherError, "unrecognised time value: #{value.inspect}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def patch_front_matter(path, at_uri)
|
|
109
|
+
content = File.read(path)
|
|
110
|
+
unless content.start_with?("---\n") || content.start_with?("---\r\n")
|
|
111
|
+
raise PublisherError, "no front matter in #{path}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
patched = content.sub(/\n---\r?\n/, %(\nat_uri: "#{at_uri}"\n---\n))
|
|
115
|
+
if patched == content
|
|
116
|
+
raise PublisherError, "could not locate closing front-matter delimiter in #{path}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
File.write(path, patched)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require "rake"
|
|
2
|
+
require "jekyll"
|
|
3
|
+
require_relative "../jekyll-standard-site"
|
|
4
|
+
require_relative "publisher"
|
|
5
|
+
|
|
6
|
+
namespace :standard_site do
|
|
7
|
+
desc "Create site.standard.document records for posts missing at_uri and patch front matter"
|
|
8
|
+
task :publish do
|
|
9
|
+
handle = ENV["BSKY_HANDLE"] or abort("BSKY_HANDLE is not set")
|
|
10
|
+
password = ENV["BSKY_APP_PASSWORD"] or abort("BSKY_APP_PASSWORD is not set")
|
|
11
|
+
pds = ENV["BSKY_PDS"] || Jekyll::StandardSite::Publisher::DEFAULT_PDS
|
|
12
|
+
|
|
13
|
+
site = Jekyll::Site.new(Jekyll.configuration({}))
|
|
14
|
+
site.read
|
|
15
|
+
|
|
16
|
+
publisher = Jekyll::StandardSite::Publisher.new(
|
|
17
|
+
site: site,
|
|
18
|
+
handle: handle,
|
|
19
|
+
password: password,
|
|
20
|
+
pds: pds
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
published = publisher.publish_all
|
|
24
|
+
if published.empty?
|
|
25
|
+
puts "Standard.site: no posts to publish"
|
|
26
|
+
else
|
|
27
|
+
puts "Standard.site: published #{published.size} document(s)"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-standard-site
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Nesbitt
|
|
@@ -85,6 +85,8 @@ files:
|
|
|
85
85
|
- LICENSE.txt
|
|
86
86
|
- README.md
|
|
87
87
|
- lib/jekyll-standard-site.rb
|
|
88
|
+
- lib/jekyll-standard-site/publisher.rb
|
|
89
|
+
- lib/jekyll-standard-site/tasks.rb
|
|
88
90
|
- lib/jekyll-standard-site/version.rb
|
|
89
91
|
homepage: https://github.com/andrew/jekyll-standard-site
|
|
90
92
|
licenses:
|