A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

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

at trunk 11 kB View raw
1--- 2import Base from '../layouts/Base.astro'; 3import Logo from '../components/Logo.astro'; 4import Footer from '../components/Footer.astro'; 5import { 6 CONTENT_LEXICON, 7 CONTENT_LEXICON_ID, 8 CONTENT_LEXICON_DESCRIPTION, 9 contentSchemaFields, 10} from '../lib/lexicon/schema-doc'; 11import { parseInlineCode } from '../lib/lexicon/inline-code'; 12import { highlightSource } from '../lib/reader/highlight'; 13 14const contentFields = contentSchemaFields(); 15const schemaJson = JSON.stringify( CONTENT_LEXICON, null, 2 ); 16// The schema is JSON we control, so highlight it directly (known language) rather 17// than auto-detecting. Server-side at build time no client JS, like the reader. 18const schemaJsonHtml = highlightSource( schemaJson, 'json' ); 19 20// A faithful, trimmed slice of a real published article's content object (the 21// "Potato buns, one way" recipe), so the page can show how the block tree 22// actually nests rather than describe it abstractly. The live record has 17 23// top-level blocks; this keeps the first heading and a two-item list, with the 24// `$type` union discriminator and `version` intact. 25const exampleContent = { 26 $type: 'blog.skypress.content.gutenberg', 27 version: 1, 28 blocks: [ 29 { 30 name: 'core/heading', 31 attributes: { level: 2, content: 'Ingredients' }, 32 innerBlocks: [], 33 }, 34 { 35 name: 'core/list', 36 attributes: { ordered: false, values: '' }, 37 innerBlocks: [ 38 { 39 name: 'core/list-item', 40 attributes: { content: '600g T45 flour' }, 41 innerBlocks: [], 42 }, 43 { 44 name: 'core/list-item', 45 attributes: { content: '200g warm milk' }, 46 innerBlocks: [], 47 }, 48 ], 49 }, 50 ], 51}; 52const exampleJson = JSON.stringify( exampleContent, null, 2 ); 53const exampleJsonHtml = highlightSource( exampleJson, 'json' ); 54--- 55 56<Base 57 title="The lexicon: how SkyPress stores your writing" 58 description="The open AT Protocol records SkyPress writes to your account when you publish: a publication, a document with your block content, and a Bluesky post linking back." 59> 60 <header class="lex-masthead"> 61 <a class="lex-home" href="/" aria-label="SkyPress home"><Logo /></a> 62 <a class="btn btn--ghost" href="/write">Start writing</a> 63 </header> 64 65 <main class="lex"> 66 <p class="lex-eyebrow">The open format</p> 67 <h1 class="lex-title">Your words live in <em>your</em> account.</h1> 68 <p class="lex-lede"> 69 When you publish with SkyPress, nothing gets locked inside SkyPress. Your 70 writing is saved to your own AT&nbsp;Protocol account, in open formats any 71 reader on the social web can understand. You can take it anywhere. Here&rsquo;s 72 exactly what we write, and why. 73 </p> 74 75 <section class="lex-section"> 76 <h2>What gets written when you publish</h2> 77 <p>Publishing creates a few records on your PDS (the data store behind your account):</p> 78 <ul class="lex-records"> 79 <li> 80 <code>site.standard.publication</code>: your SkyPress home. We create 81 it once, then reuse it. 82 </li> 83 <li> 84 <code>site.standard.document</code>: the article itself, with its 85 title, a plain-text copy, and your block content. 86 </li> 87 <li> 88 <code>app.bsky.feed.post</code>: a public Bluesky post that links back 89 to the article, so people can find it. 90 </li> 91 </ul> 92 <p> 93 The first two use the community 94 <a href="https://tangled.org/standard.site/lexicons" target="_blank" rel="noopener noreferrer">standard.site</a> 95 lexicons, not anything SkyPress-specific, so other tools and readers that 96 already speak them can pick up your writing too. The one format SkyPress 97 actually owns is the block content tucked inside the document; that&rsquo;s 98 the part below. 99 </p> 100 <p class="lex-honest"> 101 One thing worth repeating: publishing also creates a public Bluesky post. 102 We&rsquo;ll always remind you before you hit publish&nbsp;:) 103 </p> 104 </section> 105 106 <section class="lex-section"> 107 <h2>The format we own: <code>{ CONTENT_LEXICON_ID }</code></h2> 108 <p>{ parseInlineCode( CONTENT_LEXICON_DESCRIPTION ).map( ( s ) => ( s.code ? <code>{ s.text }</code> : s.text ) ) }</p> 109 <table class="lex-table"> 110 <thead> 111 <tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Required</th><th scope="col">What it is</th></tr> 112 </thead> 113 <tbody> 114 { contentFields.map( ( f ) => ( 115 <tr> 116 <td><code>{ f.name }</code></td> 117 <td>{ f.type }</td> 118 <td>{ f.required ? 'yes' : 'optional' }</td> 119 <td>{ parseInlineCode( f.description ).map( ( s ) => ( s.code ? <code>{ s.text }</code> : s.text ) ) }</td> 120 </tr> 121 ) ) } 122 </tbody> 123 </table> 124 <p class="lex-note"> 125 This is the only lexicon SkyPress owns. If a reader doesn&rsquo;t understand the 126 format, it falls back to the document&rsquo;s plain-text 127 <code>textContent</code>, so your article is never unreadable. Here&rsquo;s 128 the full schema: 129 </p> 130 <pre class="lex-code"><code class="hljs language-json" set:html={ schemaJsonHtml } /></pre> 131 132 <h3>What the blocks look like</h3> 133 <p> 134 The schema says <code>blocks</code> is &ldquo;the block tree exactly as 135 produced by the editor&rdquo;, which is a bit abstract. So here&rsquo;s a 136 real one. This is the <code>content</code> object from a published recipe, 137 <a href="/@jeherve.com/recipes/3mnwlydxcr22u">Potato buns, one way</a>, 138 trimmed to its first heading and a two-item list: 139 </p> 140 <pre class="lex-code"><code class="hljs language-json" set:html={ exampleJsonHtml } /></pre> 141 <p> 142 Every block is the same little shape: a <code>name</code> (the Gutenberg 143 block, like <code>core/heading</code>), an <code>attributes</code> object 144 for its content and settings, and <code>innerBlocks</code> for anything 145 nested inside it, which is how the list here holds its items. The same 146 shape repeats all the way down. The <code>$type</code> tells any reader 147 which format it&rsquo;s looking at; everything else is just blocks. You can 148 see the full record behind any article yourself with the 149 &ldquo;view&nbsp;record&rdquo; link in its eyebrow&nbsp;:) 150 </p> 151 </section> 152 153 <section class="lex-section"> 154 <h2>Want to run your own?</h2> 155 <p> 156 SkyPress is open source (GPL-2.0-or-later), and it&rsquo;s only ever an 157 editor, an OAuth client, and a public renderer, never a home for your 158 data. Your writing already lives in your AT&nbsp;Protocol account, in the 159 open records above, so nothing&rsquo;s tied to this particular site. 160 </p> 161 <p> 162 If you&rsquo;d rather not use skypress.blog, you can host your own copy. It&rsquo;s 163 an Astro app that deploys to Cloudflare; the only thing you need to set is a 164 <code>PUBLIC_SITE_URL</code>, and the OAuth client metadata and redirect URIs 165 adapt to wherever you put it. The code, and the setup steps, live in the repo: 166 <a href="https://tangled.org/jeremy.herve.bzh/skypress" target="_blank" rel="noopener noreferrer">tangled.org/jeremy.herve.bzh/skypress</a>. 167 Fork it, send patches, or just have a read&nbsp;:) 168 </p> 169 </section> 170 171 <div class="lex-actions"> 172 <a class="btn btn--primary" href="/write">Start writing</a> 173 </div> 174 </main> 175 176 <Footer /> 177</Base> 178 179<style> 180 .lex-masthead { 181 display: flex; 182 align-items: center; 183 justify-content: space-between; 184 padding: 1.5rem clamp( 1.25rem, 5vw, 4rem ); 185 border-bottom: 1px solid var( --line ); 186 } 187 .lex-home { text-decoration: none; } 188 189 .lex { 190 max-width: 48rem; 191 margin: 0 auto; 192 padding: clamp( 2.5rem, 7vh, 4.5rem ) clamp( 1.25rem, 5vw, 4rem ) 4rem; 193 } 194 .lex-eyebrow { 195 font-family: var( --font-mono ); 196 text-transform: uppercase; 197 letter-spacing: 0.12em; 198 font-size: 0.72rem; 199 color: var( --sun ); 200 margin: 0; 201 } 202 .lex-title { 203 font-size: clamp( 2.2rem, 6vw, 3.4rem ); 204 font-weight: 760; 205 letter-spacing: -0.03em; 206 line-height: 1.02; 207 margin: 0.5rem 0 0; 208 } 209 .lex-title em { font-style: italic; color: var( --sun ); } 210 .lex-lede { 211 font-size: clamp( 1.1rem, 2.2vw, 1.3rem ); 212 line-height: 1.6; 213 color: var( --ink-soft ); 214 margin: 1.25rem 0 0; 215 } 216 217 .lex-section { 218 margin-top: 3rem; 219 padding-top: 2.5rem; 220 border-top: 1px solid var( --line ); 221 } 222 .lex-section h2 { 223 font-size: 1.5rem; 224 letter-spacing: -0.02em; 225 margin: 0 0 0.75rem; 226 } 227 .lex-section h3 { 228 font-size: 1.1rem; 229 margin: 2rem 0 0.6rem; 230 } 231 .lex-section p { color: var( --ink-soft ); line-height: 1.65; } 232 .lex-section code { 233 font-family: var( --font-mono ); 234 font-size: 0.86em; 235 background: var( --sun-tint ); 236 color: var( --sun-strong ); 237 padding: 0.08em 0.36em; 238 border-radius: 5px; 239 } 240 /* Long NSIDs (e.g. blog.skypress.content.gutenberg) have no spaces, so let 241 them break inside headings rather than overflow narrow viewports. */ 242 .lex-section h2 code, 243 .lex-section h3 code { 244 overflow-wrap: anywhere; 245 word-break: break-word; 246 } 247 248 .lex-records { padding-left: 1.2rem; color: var( --ink-soft ); line-height: 1.7; } 249 .lex-records li { margin: 0.35rem 0; } 250 .lex-honest { color: var( --muted ); font-size: 0.95rem; } 251 .lex-note { margin-top: 1.25rem; } 252 253 .lex-table { 254 width: 100%; 255 border-collapse: collapse; 256 margin: 1rem 0 0; 257 font-size: 0.95rem; 258 } 259 .lex-table th, 260 .lex-table td { 261 text-align: left; 262 vertical-align: top; 263 padding: 0.6rem 0.75rem; 264 border-bottom: 1px solid var( --line ); 265 } 266 .lex-table th { 267 font-family: var( --font-mono ); 268 text-transform: uppercase; 269 letter-spacing: 0.04em; 270 font-size: 0.7rem; 271 color: var( --muted ); 272 } 273 .lex-table td { color: var( --ink-soft ); } 274 .lex-table td:first-child code { white-space: nowrap; } 275 276 .lex-code { 277 margin-top: 1.25rem; 278 background: var( --paper-raised ); 279 border: 1px solid var( --line ); 280 border-radius: 10px; 281 padding: 1rem 1.1rem; 282 overflow: auto; 283 font-size: 0.85rem; 284 line-height: 1.55; 285 /* String/number hues that read on the light code surface; the warm-orange 286 and muted token colours below reuse brand tokens that already flip. */ 287 --code-string: #3f7d4f; 288 --code-number: #2f6f86; 289 } 290 .lex-code code { 291 font-family: var( --font-mono ); 292 background: none; 293 color: var( --ink ); 294 padding: 0; 295 } 296 /* highlight.js token colours, scoped to this light code block (unlike the 297 reader's dark <pre>). The spans arrive via set:html, so they need :global. 298 No background/padding on .hljs, so the .lex-code chrome above is untouched. */ 299 .lex-code :global( .hljs-comment ), 300 .lex-code :global( .hljs-quote ), 301 .lex-code :global( .hljs-punctuation ) { 302 color: var( --muted ); 303 } 304 .lex-code :global( .hljs-comment ), 305 .lex-code :global( .hljs-quote ) { 306 font-style: italic; 307 } 308 .lex-code :global( .hljs-attr ), 309 .lex-code :global( .hljs-keyword ), 310 .lex-code :global( .hljs-selector-tag ), 311 .lex-code :global( .hljs-name ) { 312 color: var( --sun-strong ); 313 } 314 .lex-code :global( .hljs-string ), 315 .lex-code :global( .hljs-symbol ), 316 .lex-code :global( .hljs-meta .hljs-string ) { 317 color: var( --code-string ); 318 } 319 .lex-code :global( .hljs-number ), 320 .lex-code :global( .hljs-literal ), 321 .lex-code :global( .hljs-built_in ) { 322 color: var( --code-number ); 323 } 324 325 @media ( prefers-color-scheme: dark ) { 326 .lex-code { 327 --code-string: #8fc98f; 328 --code-number: #7fb2c9; 329 } 330 } 331 332 .lex-actions { 333 display: flex; 334 gap: 0.85rem; 335 flex-wrap: wrap; 336 margin-top: 3rem; 337 padding-top: 2.5rem; 338 border-top: 1px solid var( --line ); 339 } 340</style>