diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..925e451 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: release +on: + push: + branches: + - main +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set outputs + id: vars + run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + - uses: actions/setup-go@v4 + with: + go-version: '1.21' + - name: build docs site + run: make ssg + - name: publish to pgs + uses: picosh/pgs-action@v3 + with: + user: hey + key: ${{ secrets.PRIVATE_KEY }} + src: './public/' + project: "docs-${{ steps.vars.outputs.sha_short }}" + promote: "docs-prod" + retain: "docs-" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02310cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +public/* +!public/.gitkeep +*.swp +*.log +.DS_Store diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8019a43 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +clean: + rm -rf ./public/* + echo "" > ./public/.gitkeep +.PHONY: clean + +ssg: + go run ./main.go + cp ./static/* ./public +.PHONY: ssg + +dev: ssg + rsync -vr ./public/ hey@pgs.sh:/docs-local +.PHONY: dev diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bc38baf --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/picosh/docs + +go 1.21.5 + +require github.com/picosh/pdocs v0.0.0-20240108145604-582bbd2bcc23 + +require ( + github.com/alecthomas/chroma v0.10.0 // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/yuin/goldmark v1.6.0 // indirect + github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 // indirect + github.com/yuin/goldmark-meta v1.1.0 // indirect + go.abhg.dev/goldmark/anchor v0.1.1 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d5a3ad4 --- /dev/null +++ b/go.sum @@ -0,0 +1,31 @@ +github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= +github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/picosh/pdocs v0.0.0-20240108145604-582bbd2bcc23 h1:gnUGSuxipJYPRyJlvgKOxWEgKDPkCWvF5b9duXJTxBM= +github.com/picosh/pdocs v0.0.0-20240108145604-582bbd2bcc23/go.mod h1:rh8n5EosoD8svAbVPUEuXWfcWBsGj3GPeG/8D/0Az3c= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.4.5/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= +github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= +github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 h1:yHfZyN55+5dp1wG7wDKv8HQ044moxkyGq12KFFMFDxg= +github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594/go.mod h1:U9ihbh+1ZN7fR5Se3daSPoz1CGF9IYtSvWwVQtnzGHU= +github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= +github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= +go.abhg.dev/goldmark/anchor v0.1.1 h1:NUH3hAzhfeymRqZKOkSoFReZlEAmfXBZlbXEzpD2Qgc= +go.abhg.dev/goldmark/anchor v0.1.1/go.mod h1:zYKiaHXTdugwVJRZqInVdmNGQRM3ZRJ6AGBC7xP7its= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..f260016 --- /dev/null +++ b/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "github.com/picosh/pdocs" +) + +func main() { + pager := pdocs.Pager("./posts") + sitemap := []*pdocs.Sitemap{ + { + Text: "Marketing", + Href: "/", + Page: pager("marketing.md"), + }, + { + Text: "Getting Started", + Href: "/getting-started", + Page: pager("getting-started.md"), + }, + { + Text: "How it Works", + Href: "/how-it-works", + Page: pager("how-it-works.md"), + }, + { + Text: "File Uploads", + Href: "/file-uploads", + Page: pager("file-uploads.md"), + }, + { + Text: "Custom Domains", + Href: "/custom-domains", + Page: pager("custom-domains.md"), + }, + { + Text: "Pages", + Href: "/pgs", + Page: pager("pgs.md"), + }, + { + Text: "Prose", + Href: "/prose", + Page: pager("prose.md"), + }, + { + Text: "Pastes", + Href: "/pastes", + Page: pager("pastes.md"), + }, + { + Text: "Images", + Href: "/imgs", + Page: pager("imgs.md"), + }, + { + Text: "Feeds", + Href: "/feeds", + Page: pager("feeds.md"), + }, + { + Text: "Lists", + Href: "/lists", + Page: pager("lists.md"), + }, + { + Text: "Tunnels", + Href: "/tuns", + Page: pager("tuns.md"), + }, + { + Text: "Community", + Href: "/comms", + Page: pager("comms.md"), + }, + { + Text: "FAQ", + Href: "/faq", + Page: pager("faq.md"), + }, + { + Text: "Plain Text Lists", + Href: "/plain-text-lists", + Page: pager("plain-text-lists.md"), + }, + { + Text: "Operations", + Href: "/ops", + Page: pager("ops.md"), + }, + { + Text: "Privacy Policy", + Href: "/privacy", + Page: pager("privacy.md"), + }, + } + + config := &pdocs.DocConfig{ + Sitemap: sitemap, + Out: "./public", + Tmpl: "./tmpl", + PageTmpl: "post.page.tmpl", + } + + err := config.GenSite() + if err != nil { + panic(err) + } +} diff --git a/posts/comms.md b/posts/comms.md new file mode 100644 index 0000000..604f20d --- /dev/null +++ b/posts/comms.md @@ -0,0 +1,53 @@ +--- +title: Community +description: How we interact with our users +keywords: [pico, irc, bouncer] +--- + +All of our realtime communication happens through IRC at #pico.sh @ libera.chat. + +We are also excited to announce that we have a hosted IRC bouncer and web client +that all pico users can use. + +- [soju man page](https://soju.im/doc/soju.1.html) +- [bouncer: ircs://irc.pico.sh:6697](ircs://irc.pico.sh:6697) +- [web client: chat.pico.sh](https://chat.pico.sh) + +## Generate a login token for bouncer + +![pico-token-menu](https://hey.imgs.sh/pico-token-menu.png) + +- SSH into a pico service CMS (e.g. `ssh prose.sh`) +- Select "tokens" submenu +- Type "n" to generate a new token +- Save token someplace safe + +## Log into [chat.pico.sh](https://chat.pico.sh) + +- You'll be redirected to [auth.pico.sh](https://auth.pico.sh) which implements + a fake oauth2 service +- Enter the token from above +- Click submit + +## Back to [chat.pico.sh](https://chat.pico.sh) + +![irc-remember-me](https://hey.imgs.sh/irc-remember-me/x500) + +- Click "remember me" (this is important) +- You'll see an error "Cannot interact with channels and users on the bouncer + connection. Did you mean to use a specific network?" that's okay +- Message `BouncerServ` (`/msg BouncerServ help`) to configure the bouncer + +## Connecting to `irc.libera.chat/#pico.sh` + +- `/msg BouncerServ help` +- `network create -addr irc.libera.chat -name libera -nick -enabled false` +- `sasl set-plain -network libera ` +- `network update libera -enabled true` +- `/j #pico.sh` +- Join any other channels or networks using the same method! + +# Acknowledgments + +- [soju (bouncer)](https://git.sr.ht/~emersion/soju) +- [gamja (web client)](https://git.sr.ht/~emersion/gamja) diff --git a/posts/custom-domains.md b/posts/custom-domains.md new file mode 100644 index 0000000..32c2009 --- /dev/null +++ b/posts/custom-domains.md @@ -0,0 +1,96 @@ +--- +title: Custom Domains +description: A guide to setting up your domain to point to pico services +keywords: [pico, custom, domain] +--- + +All of our services support custom domains and they all work the exact same way. +The way it works is you provide a `CNAME` record and a corresponding `TXT` +record. Then when any of our web services receives traffic from that domain, we +check the `TXT` record to figure out what content to serve the user. + +HTTPS will be automatically enabled and a certificate will be retrieved from +Let's Encrypt. In order for this to work, 2 DNS records need to be created: + +`CNAME` for the domain to the pico service (subdomains or DNS hosting with CNAME +flattening) or `A` record. + +## For prose.sh + +``` +CNAME subdomain.yourcustomdomain.com -> prose.sh +``` + +Resulting in: + +``` +subdomain.yourcustomdomain.com. 300 IN CNAME prose.sh. +``` + +And a `TXT` record to tell Prose what blog is hosted on that domain at the +subdomain entry `_prose` + +``` +TXT _prose.subdomain.yourcustomdomain.com -> yourproseusername +``` + +Resulting in: + +``` +_prose.subdomain.yourcustomdomain.com. 300 IN TXT "hey" +``` + +Depending on your DNS, this could take some time to fully switch over. We have +an endpoint to check whether or not custom domains are setup: + +``` +curl -vvvv https://prose.sh/check?domain=xxx +``` + +## For pgs.sh + +[pgs.sh](https://pgs.sh) is a little different in that we allow the user to +configure custom domains per project so it's a little different. + +And a `TXT` record to tell pgs what project is hosted on that domain at the +subdomain entry `_pgs`. + +``` +subdomain.yourcustomdomain.com. 300 IN CNAME pgs.sh. +_pgs.subdomain.yourcustomdomain.com. 300 IN TXT +"{user}-{project}" +``` + +### Example: top-level domain + +- Custom domain `erock.io` +- User `erock` +- Project `kittens` + +Resulting in: + +``` +erock.io. 300 IN CNAME pgs.sh. +_pgs.erock.io. 300 IN TXT "erock-kittens" +``` + +### Example: subdomain + +- Custom domain `meow.erock.io` +- User `erock` +- Project `kittens` + +Resulting in: + +``` +meow.erock.io. 300 IN CNAME pgs.sh. +_pgs.meow.erock.io. 300 IN TXT "erock-kittens" +``` + +## My DNS does **not** support CNAME flattening + +In that case, you need to get the IP address of the service you want to point to +and then use that as an `A` record. + +> WARNING: We make no gaurentees that our IP addresses will stay the same. Use +> at your own risk! diff --git a/posts/faq.md b/posts/faq.md new file mode 100644 index 0000000..ef22034 --- /dev/null +++ b/posts/faq.md @@ -0,0 +1,47 @@ +--- +title: Frequently Asked Questions +description: Answers to frequently asked questions +keywords: [pico, faq, question, answer] +--- + +## Permission denied when using SSH + +Unfortunately SHA-2 RSA keys are not currently supported. + +Unfortunately, due to a shortcoming in Go’s x/crypto/ssh package, we do not +currently support access via new SSH RSA keys: only the old SHA-1 ones will +work. Until we sort this out you’ll either need an SHA-1 RSA key or a key with +another algorithm, e.g. Ed25519. Not sure what type of keys you have? You can +check with the following: + +```bash +find ~/.ssh/id_*.pub -exec ssh-keygen -l -f {} \; +``` + +If you’re curious about the inner workings of this problem have a look at: + +- [golang/go#37278](https://github.com/golang/go/issues/37278) +- [go-review](https://go-review.googlesource.com/c/crypto/+/220037) +- [golang/crypto#197](https://github.com/golang/crypto/pull/197) + +## Generating a new SSH key + +[Github Reference](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent) + +```bash +ssh-keygen -t ed25519 -C "your_email@example.com" +``` + +When you're prompted to "Enter a file in which to save the key," press Enter. +This accepts the default file location. At the prompt, type a secure passphrase. + +## Can I create multiple accounts? + +Yes! You can either: + +- a) create a new keypair and use that for authentication or +- b) use the same keypair and ssh into our CMS using our special username + `ssh new@prose.sh` + +Please note that if you use the same keypair for multiple accounts, you will +need to always specify the user when logging into our CMS. diff --git a/posts/feeds.md b/posts/feeds.md new file mode 100644 index 0000000..908ed9e --- /dev/null +++ b/posts/feeds.md @@ -0,0 +1,72 @@ +--- +title: feeds.sh +description: guide to using feeds.sh +keywords: [pico, feeds] +--- + +## Features + +- Receive email digests for your RSS feeds +- We try to render all content within the feed as HTML (with ability to disable + it) +- Create 1-to-many email digests +- Set digest interval from `10min` to `30day` + +## Subscribe to feeds + +Use our [plain text lists spec](/plain-text-lists) to create a txt file named +`daily.txt` (as an example). + +Then add your email, the digest interval, and the rss feeds for which you want +to receive email notifications. + +``` +=: email rss@myemail.com +=: digest_interval 1day +=> https://hey.prose.sh/rss +=> https://hey.lists.sh/rss +=> https://erock.prose.sh/rss +``` + +Then copy the file to our server + +```bash +rsync daily.txt feeds.sh:/ +``` + +## Privacy + +We don't do anything with your email besides send an email digest. If you delete +the post containing your email address, we no longer have you email address. + +Posts are also not accessible by the public and we provide no endpoints to view +these posts. + +## Digest interval options + +``` +10min +1hour +6hour +12hour +1day +7day +30day +``` + +## Can I create multiple email digests? + +You are free to upload as many email digests as you like, referencing different +rss feeds, emails, and digest intervals. + +## Inline content + +By default we attempt to render all content within a feed as HTML inside an +email digest. Sometimes users just want us to send them the links so they can +click on it and read the content on the original website. + +If you don't want to see all the content, simply add a variable to your post: + +``` +=: inline_content false +``` diff --git a/posts/file-uploads.md b/posts/file-uploads.md new file mode 100644 index 0000000..b292c37 --- /dev/null +++ b/posts/file-uploads.md @@ -0,0 +1,80 @@ +--- +title: File uploads +description: How to upload and download files from pico services +keywords: [pico, file, upload, download] +--- + +## How do I upload files? + +Unless otherwise specified, all of our services support the following ways to +upload files to our services. + +### rsync + +```bash +rsync hello-world.md {user}@{service}:/ +``` + +### scp + +```bash +scp hello-world.md {user}@{service}:/ +``` + +### sftp + +```bash +sftp {user}@{service} + +sftp> ls +hello-world.md + +sftp> rm hello-world.md + +sftp> put hello-world.md +``` + +## How do I update files? + +> You do **not** need to re-upload **all** of your files everytime you make a +> change to a single post or file. + +All you have to do to update a post or file is re-upload the file using the same +tools previously explained. + +## How do I delete files? + +### sftp + +The easiest way to delete a file is via [sftp](#sftp). + +### 0-byte file + +Because `scp` does not natively support deleting files, we didn't want to bake +that behavior into my ssh server. + +However, if a user wants to delete a post they can delete the contents of the +file and then upload it to our server. If the file contains 0 bytes, we will +remove the post. For example, if you want to delete `delete.md` you could: + +```bash +cp /dev/null delete.md +scp ./delete.md prose.sh:/ +``` + +### CMS + +Alternatively, most of our services have a way to delete posts via the SSH App +CMS. For example, if you want to delete a post on prose: + +```bash +ssh prose.sh +# Click "Manage Posts" +# Highlight the post you want to delete +# Press "X" +``` + +## How do I download files? + +Using the same tools describe [here](#how-do-i-upload-files) just reverse the +order of `src` and `dest`! diff --git a/posts/getting-started.md b/posts/getting-started.md new file mode 100644 index 0000000..9a3060a --- /dev/null +++ b/posts/getting-started.md @@ -0,0 +1,40 @@ +--- +title: Getting Started +description: How to use pico services +keywords: [pico, getting, started] +--- + +> All of our services use the same database, so when you create an account with +> one, you also have access to all of our other services. + +## Create your account with Public-Key Cryptography + +We don't need your email address. + +To get started, simply ssh into any service's CMS. + +```bash +ssh new@prose.sh +``` + +> Note: `new` is a special username that will always send you to account +> creation, even with multiple accounts associated with your key-pair. + +All we need to create an account is your username. This username will be used +for all of your service domains. For example, if your username is `glossy`, we +will create the following domains on your behalf: + +``` +glossy.prose.sh +glossy.pgs.sh +glossy.pastes.sh +glossy.imgs.sh +glossy.lists.sh +``` + +After that, just set a username and you're ready to start using our services! +When you SSH again, use your username that you set in the CMS. + +```bash +ssh glossy@prose.sh +``` diff --git a/posts/how-it-works.md b/posts/how-it-works.md new file mode 100644 index 0000000..fb0d514 --- /dev/null +++ b/posts/how-it-works.md @@ -0,0 +1,29 @@ +--- +title: How it Works +description: A brief description of how our services work +keywords: [pico, how, it, works] +--- + +The special sauce of all of our pico services is how we let users publish +changes to their sites without needing to install anything. We accomplish this +with what is colloquelly terms "SSH Apps." + +By using the SSH protocol and golang's implementation of SSH, we can create +golang binaries that interface with SSH in unique ways. + +[Charm's wish](https://github.com/charmbracelet/wish) golang library the +underlying library we use to enable all our SSH apps to work seamlessly with SSH +clients. + +Whenever a user uploads a file to our SSH app, we don't actually store anything +on our VM from the user. Instead we hold onto that file upload and put it inside +our database -- or object store depending on the service. + +We support a few clients for file uploads: + +- `scp` +- `sftp` +- `rsync` + +All of these are implemented using `wish` and our +[own golang library](https://github.com/picosh/send). diff --git a/posts/imgs.md b/posts/imgs.md new file mode 100644 index 0000000..e1f00c4 --- /dev/null +++ b/posts/imgs.md @@ -0,0 +1,81 @@ +--- +title: imgs.sh +description: guide to using imgs.sh +keywords: [pico, imgs] +--- + +## Features + +- Delightful terminal workflow +- Share public images from the terminal +- Seamless integration with other pico services (e.g. prose) +- Images are web optimized by default +- API to modify images on-the-fly (e.g. dimensions) +- Hotlinking encouraged! +- No javascript +- No ads +- 10MB max image size +- 1GB max storage + +## What it is + +- A place to host **public** images +- A place to host your blog images +- We moderate all images + +## What it isn't + +- Not a general cloud photo storage solution +- Not a place to backup or store your photos + +## Publish your images with one command + +When your image is ready to be published, copy the file to our server with a +familiar command: + +```bash +rsync *.jpg imgs.sh:/ +``` + +We'll either create or update the images for you. + +## Web optimized + +When a user uploads an image, we immediately convert it to `webp`. Then we have +an API that serves those web optimized images. + +## How does imgs integrate with other pico services? + +We allow any of our other services to upload images from those services to imgs. +For example, if you want to upload images for prose.sh all you have to do is +include your images in the rsync or scp command. + +```bash +rsync profile.jpg imgs.sh:/ +``` + +Then when you want to reference the file, you can reference it like so: + +```md +![profile pic](/profile.jpg) +``` + +## What file types are supported? + +``` +jpg +png +gif +webp +svg +``` + +## Image manipulation + +We have an API that allows users to resize images on-the-fly. Currently we only +support downscaling. + +```md +[!profile](/profile/x500) # auto scale width [!profile](/profile/500x500) # +scale width and height [!profile](/profile/500x) # auto scale height +``` diff --git a/posts/lists.md b/posts/lists.md new file mode 100644 index 0000000..c247ade --- /dev/null +++ b/posts/lists.md @@ -0,0 +1,74 @@ +--- +title: lists.sh +description: guide to using lists.sh +keywords: [pico, lists] +--- + +## Features + +- Just lists +- Looks great on any device +- Bring your own editor +- You control the source files +- Terminal workflow with no installation +- Public-key based authentication +- No ads, zero browser-based tracking +- No platform lock-in +- No javascript +- Subscriptions via RSS +- Not a platform for todos +- Minimalist design +- 100% open source + +## Publish your posts with one command + +When your post is ready to be published, copy the file to our server with a +familiar command: + +```bash +rsync ~/lists/*.txt lists.sh:/ +``` + +We'll either create or update the lists for you. + +## Plain text format + +A [simple specification](/plain-text-lists) that is flexible and with no frills. + +## How do I change my blog's name? + +All you have to do is create a post titled `_header.txt` and add some +information to the list. + +``` +=: title My new blog! +=: description My blog description! +=> https://xyz.com website +=> https://twitter.com/xyz twitter +``` + +## How do I add an introduction to my blog? + +All you have to do is create a post titled `_readme.txt` and add some +information to the list. + +``` +=: list_type none +# Hi my name is Bob! +I like to sing. Dance. And I like to have fun fun fun! +``` + +Whatever is inside the `_readme` file will get rendered (as a list) right above +your blog posts. Neat! + +## How can I change the layout of my blog? + +Inside the `_header.txt` metadata file, there's a variable `layout` option that +will change the layout of your blog index page. + +Currently supported options: + +``` +=: layout default +=: layout aside +``` diff --git a/posts/marketing.md b/posts/marketing.md new file mode 100644 index 0000000..68a16f9 --- /dev/null +++ b/posts/marketing.md @@ -0,0 +1,6 @@ +--- +title: pico.sh +description: Providing tools and services to improve communication and collaboration on the web +slug: index +template: marketing.page.tmpl +--- diff --git a/posts/ops.md b/posts/ops.md new file mode 100644 index 0000000..49d1be2 --- /dev/null +++ b/posts/ops.md @@ -0,0 +1,76 @@ +--- +title: Operations +description: pico operations +keywords: [pico, operation, ops] +--- + +## Purpose + +Our services exist to allow people to create and share their thoughts without +the need to set up their own server or be part of a platform that shows ads or +tracks its users. Ethics + +We are committed to: + +- No browser-based tracking of visitor behavior. +- No attempt to identify users. +- Never sell any user or visitor data. +- No ads — ever. + +## Code of Content Publication + +Content in pico.sh services is unfiltered and unmonitored. Users are free to +publish any combination of words and pixels except for: content of animosity or +disparagement of an individual or a group on account of a group characteristic +such as race, color, national origin, sex, disability, religion, or sexual +orientation, which will be taken down immediately. + +If one notices something along those lines in a blog please let us know at +hello@pico.sh. + +## Liability + +The user expressly understands and agrees that pico.sh, the operator of this +website shall not be liable, in law or in equity, to them or to any third party +for any direct, indirect, incidental, lost profits, special, consequential, +punitive or exemplary damages. + +## Analytics + +We are committed to zero browser-based tracking or trying to identify visitors. +This means we do not try to understand the user based on cookies or IP address. +We do not store personally identifiable information. + +However, in order to provide a better service, we do have some analytics on +content. List of metrics we track for content: + +- anonymous view counts + +We might also inspect the headers of HTTP requests to determine some tertiary +information about the request. For example we might inspect the User-Agent or +Referer to filter out requests from bots. + +## Account Terms + +The user is responsible for all content posted and all actions performed with +their account. We reserve the right to disable or delete a user's account for +any reason at any time. We have this clause because, statistically speaking, +there will be people trying to do something nefarious. + +## Service Availability + +We provide pico.sh services on an "as is" and "as available" basis. We do not +offer service-level agreements but do take uptime seriously. + +## Contact and Support + +Email us at hello@pico.sh with any questions. + +## Acknowledgments + +In particular we would like to thank: + +- The charm.sh community +- The golang community +- The postgresql community +- The caddy community diff --git a/posts/pastes.md b/posts/pastes.md new file mode 100644 index 0000000..ea8d849 --- /dev/null +++ b/posts/pastes.md @@ -0,0 +1,53 @@ +--- +title: pastes.sh +description: guide to using pastes.sh +keywords: [pico, pastes] +--- + +## Features + +- Pastes last 90 days by default +- [Ability to set custom expiration](#how-do-i-set-expiration-date) +- [Ability to "hide" pastes](#how-do-i-unlist-a-paste) +- Bring your own editor +- Terminal workflow with no installation +- Use sftp to manage pastes +- Public-key based authentication +- No ads, zero tracking +- No javascript +- Minimalist design +- 100% open source + +## Pipe Support + +```bash +echo "foobar" | ssh pastes.sh + +echo "foobar" | ssh pastes.sh FILENAME + +# if the tty warning annoys you +echo "foobar" | ssh -T pastes.sh +``` + +## How do I set expiration date? + +Yes. The default expiration date for a paste is 90 days. We do allow the user to +set the paste to never expire. We also allow custom duration or timestamp. + +```bash +echo "foobar" | ssh pastes.sh FILENAME expires=false + +echo "foobar" | ssh pastes.sh FILENAME expires=2023-12-12 + +echo "foobar" | ssh pastes.sh FILENAME expires=1h +``` + +## How do I unlist a paste? + +Yes. Unlisted in this context means it does not show up on your user landing +page where we show all of your pastes. In this case, yes, you can "hide" it +using a pipe command. + +```bash +echo "foobar" | ssh pastes.sh FILENAME hidden=true +``` diff --git a/posts/pgs.md b/posts/pgs.md new file mode 100644 index 0000000..f3c1eaf --- /dev/null +++ b/posts/pgs.md @@ -0,0 +1,162 @@ +--- +title: pgs.sh +description: guide to using pgs.sh +keywords: [pico, pgs] +--- + +## Closed beta + +Anyone can get an invite! + +The only requirement is to stay in our IRC channel and be willing to provide +feedback on how we can improve the service. + +Join our IRC channel +[#pico.sh @ libera.chat](https://web.libera.chat/gamja?autojoin=#pico.sh) and +ask for an invite. + +## Features + +- Terminal workflow +- No client-side installation required to fully manage static sites +- Distinct static sites as projects +- Unlimited projects created on-the-fly (no need to create a project first) +- Deploy using `rsync`, `scp`, or `sftp` +- Promotion/rollback support (via symbolic linking from one project to another) +- Managed HTTPS for all projects (e.g. `https://erock-myproject.pgs.sh`) +- [Custom domains](/custom-domains) for projects (managed simply by `TXT` + records) +- [User-defined redirects](#user-defined-redirects) +- [SPA support](#single-page-applications) +- 1GB max storage +- 50MB max file size +- All assets are public-only +- [Only web assets are supported](#what-file-types-are-supported) + +## Publish your site with one command + +When your site is ready to be published, copy the files to our server with a +familiar command: + +```bash +rsync -rv public/ pgs.sh:/myproj +``` + +That's it! There's no need to formally create a project, we create them +on-the-fly. Further, we provide TLS for every project automatically. In this +case the url for the project above would look something like +`https://{username}-myproj.pgs.sh`. + +## Project promotion and rollbacks + +Additionally you can setup a pipeline for promotion and rollbacks, which will +instantly update your project. + +```bash +ssh pgs.sh link project-prod project-d0131d4 +``` + +A common way to perform promotions within pgs.sh is to setup CI/CD so every push +to main would trigger a build and create a new project based on the git commit +hash (e.g. `project-d0131d4`). + +This command will create a symbolic link from `project-prod` to +`project-d0131d4`. Want to rollback a release? Just change the link for +`project-prod` to a previous project. + +We also built a [github action](https://github.com/picosh/pgs-action) that +handles all the logic for uploading to pgs.sh. +[Here's an example of it in action.](https://erock-git-neovimcraft.pgs.sh/tree/main/item/.github/workflows/deploy.yml.html#27) + +## Manage your projects with a remote CLI + +Our management system is done via ssh commands. + +> Our CLI commands are currently in active development so the list of available +> commands are changing. + +The best way to learn about all the commands we support is via an SSH command: + +```bash +ssh pgs.sh help +``` + +Having said that, we do want to demonstrate the power of pgs.sh by discussing +design goals. All of our SSH commands are safe-by-default. Meaning, they never +mutate server state by default. This provides users an opportunity to experiment +with our commands to see how they work. In order to actually trigger server +mutations, every command must be appended with `--write`. + +Further, we want to make sure users are able to manage their static sites +exclusively from SSH commands. Below is list of features we support via SSH +commands: + +```bash +# storage usage stats +ssh pgs.sh stats + +# list all project (and their links) +ssh pgs.sh ls + +# list all project dependencies +ssh pgs.sh depends project-x + +# link a project +ssh pgs.sh link project-x project-y + +# unlink a project +ssh pgs.sh unlink project-x + +# delete a project +ssh pgs.sh rm project-x + +# delete all projects matching a prefix +# (except projects that have linked projects) +ssh pgs.sh prune prefix + +# delete all projects matching a prefix +# except the latest (3) projects +ssh pgs.sh retain prefix +``` + +## What file types are supported? + +``` +html +htm +css +js +jpg +png +gif +webp +svg +ico +pdf +json +txt +otf +ttf +woff +woff2 +md +rss +xml +atom +map +webmanifest +``` + +## User-defined Redirects + +We support custom redirects via a special file `_redirects`. + +Read more about it at [netflify](https://docs.netlify.com/routing/redirects). + +## Single-Page Applications + +pgs supports SPAs! Upload a `_redirects` file your project: + +``` +/* /index.html 200 +``` diff --git a/posts/plain-text-lists.md b/posts/plain-text-lists.md new file mode 100644 index 0000000..8b41c81 --- /dev/null +++ b/posts/plain-text-lists.md @@ -0,0 +1,165 @@ +--- +title: Plain Text Lists +description: Our spec for plain text lists +keywords: [pico, lists, spec, plain, text] +--- + +- Version: **2022.08.05.dev** +- Status: **Draft** +- Author: **Eric Bower** + +The goal of this specification is to understand how we render plain text lists. +The overall design of this format is to be easy to parse and render. + +The format is line-oriented, and a satisfactory rendering can be achieved with a +single pass of a document, processing each line independently. As per gopher, +links can only be displayed one per line, encouraging neat, list-like structure. + +Feedback on any part of this is extremely welcome, please email +[hello@pico.sh](mailto:hello@pico.sh). + +The source code for our parser can be found +[here](https://github.com/picosh/pico/blob/85ad4b81370427925328ab24fa568f044fd624ab/shared/listparser.go). + +## Parameters + +As a subtype of the top-level media type "text", "text/plain" inherits the +"charset" parameter defined in +[RFC 2046](https://datatracker.ietf.org/doc/html/rfc2046#section-4.1). The +default value of "charset" is "UTF-8" for "text" content. + +## Line orientation + +As mentioned, the text format is line-oriented. Each line of a document has a +single "line type". It is possible to unambiguously determine a line's type +purely by inspecting its first (3) characters. A line's type determines the +manner in which it should be presented to the user. Any details of presentation +or rendering associated with a particular line type are strictly limited in +scope to that individual line. + +## File extension + +Plain text lists only supports the `.txt` file extension and will ignore all +other file extensions. + +## List item + +List items are separated by newline characters `\n`. Each list item is on its +own line. A list item does not require any special formatting. A list item can +contain as much text as it wants. We encourage soft wrapping for readability in +your editor of choice. Hard wrapping is not permitted as it will create a new +list item. + +Empty lines will be completely removed and not rendered to the end user. + +## Hyperlinks + +Hyperlinks are denoted by the prefix `=>`. The following text should then be the +hyperlink. + +``` +=> https://lists.sh +``` + +Optionally you can supply the hyperlink text immediately following the link. + +``` +=> https://lists.sh microblog for lists +``` + +## Nested lists + +Users can create nested lists. Tabbing a list will nest it under the list item +directly above it. Both tab character `\t` or whitespace as tabs are permitted. +Do not mix tabs and spaces because the nesting will yield unexpected results. + +``` +first item + second item + third item +last item +``` + +## Images + +List items can be represented as images by prefixing the line with `=<`. + +``` +=< https://i.imgur.com/iXMNUN5.jpg +``` + +Optionally you can supply the image alt text immediately following the link. + +``` +=< https://i.imgur.com/iXMNUN5.jpg I use arch, btw +``` + +## Headers + +List items can be represented as headers. We support two headers currently. +Headers will end the previous list and then create a new one after it. This +allows a single document to contain multiple lists. + +``` +# Header One +## Header Two +``` + +## Blockquotes + +List items can be represented as blockquotes. + +``` +> This is a blockquote. +``` + +## Preformatted + +List items can be represented as preformatted text where newline characters are +not considered part of new list items. They can be represented by prefixing the +line with three backticks + +```` +``` +#!/usr/bin/env bash + +set -x + +echo "this is a preformatted list item! +``` +```` + +You must also close the preformatted text with another backtick on its own line. +The next example with **NOT** work. + +```` +``` +#!/usr/bin/env bash + +echo "This will not render properly"``` +```` + +## Variables + +Variables allow us to store metadata within our system. Variables are list items +with key value pairs denoted by `=:` followed by the key, a whitespace +character, and then the value. + +``` +=: publish_at 2022-04-20 +``` + +These variables will not be rendered to the user inside the list. + +List of available variables: + +``` +=: title Hello World +=: description A fine description +=: publish_at 2022-04-20 +=: tags feature, announcement +=: list_type none +``` + +`list_type` value gets sent directly to css property +[list-style-type](https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type) diff --git a/posts/privacy.md b/posts/privacy.md new file mode 100644 index 0000000..14a9688 --- /dev/null +++ b/posts/privacy.md @@ -0,0 +1,33 @@ +--- +title: Privacy Policy +description: Details on our privacy and security approach. +keywords: [pico, privacy, policy] +--- + +## Account Data + +In order to have a functional account at pico.sh, we need to store your public +key. That is the only piece of information we record for a user. + +Because we use public-key cryptography, our security posture is a battle-tested +and proven technique for authentication. + +We also might ask for your email address in order to support some features of +our services. We will not share your email address with anyone, ever. We will +only email you when: + +- You ask us to +- Service interuptions +- Service availability + +## Third parties + +We have a strong commitment to never share any user data with any third-parties. + +## Service Providers + +We host our server on oracle cloud. + +## Cookies + +We do not use any cookies, not even account authentication. diff --git a/posts/prose.md b/posts/prose.md new file mode 100644 index 0000000..2512b3e --- /dev/null +++ b/posts/prose.md @@ -0,0 +1,156 @@ +--- +title: prose.sh +description: guide to using prose.sh +keywords: [pico, prose] +--- + +## Features + +- Github flavor markdown +- [Custom domains](/custom-domains) +- Looks great on any device +- Bring your own editor +- You control the source files +- Terminal workflow with no installation +- Public-key based authentication +- Use sftp to manage blog +- No ads, zero browser-based tracking +- No attempt to identify users +- No platform lock-in +- No javascript +- Subscriptions via RSS +- Minimalist design +- 100% open source + +## You control the source files + +Create posts using your favorite editor in plain text files. + +`~/blog/hello-world.md` + +```md +# hello world! + +This is my first blog post. + +Check out some resources: + +- [pico.sh](https://pico.sh) +- [lists.sh](https://lists.sh) +- [antoniomika](https://antoniomika.me) +- [erock.io](https://erock.io) + +Cya! +``` + +## Publish your posts with one command + +When your post is ready to be published, copy the file to our server with a +familiar command: + +``` +rsync ~/blog/* prose.sh:/ +``` + +We'll either create or update the posts for you. + +## Terminal workflow without installation + +Since we are leveraging tools you already have on your computer (`ssh` and +`rsync`), there is nothing to install. + +This provides the convenience of a web app, but from inside your terminal! + +## Upload images for your blog + +We also support image uploading (jpg, png, gif, webp, svg). Simply upload your +images alongside your markdown files and then reference them from root `/`: + +```md +--- +title: hello world! +--- + +![profile pic](/profile.jpg) +``` + +```bash +rsync ~/blog/*.jpg prose.sh:/ +``` + +## Metadata + +We support adding frontmatter to the top of your markdown posts. A frontmatter +looks like the following: + +``` +--- +title: some title! +description: this is a great description +date: 2022-06-28 +tags: feature, announcement +image: og_image.jpg +card: summary # or summary_large_image +draft: true +--- +``` + +## How can I add a footer to all of my posts? + +We have a special file `_footer.md` that will be appended to every single blog +post. + +There is nothing that differentiates itself from the rest of the post so it's up +to you to style it. For convenience we added an `id` to the containing element +`post-footer`. + +## How can I customize my blog page? + +There's a special file you can upload `_readme.md` which will allow users to add +a bio and links to their blog landing page. + +``` +--- +title: some title! +description: this is a great description +nav: + - google: https://google.com + - site: https://some.site +image: og_image.jpg +card: summary # or summary_large_image +favicon: favicon.ico +aliases: + - 2023/03/10/my-post +layout: aside # or default +--- +``` + +## How can I change the theme of my blog? + +There's a special file you can upload `_styles.css` which will allow users to +add a CSS file to their page. It will be the final CSS file loaded on the page +so it will overwrite whatever styles have previously been added. We've also +added a couple of convenience id's attached to the body element for the blog and +post pages. + +```css +/* _styles.css */ +#post { + color: green; +} + +#blog { + color: tomato; +} +``` + +## How can I change the layout of my blog? + +Inside the `_readme.md` metadata file, there's a variable layout option that +will change the layout of your blog index page. + +``` +--- +layout: aside # or default +--- +``` diff --git a/posts/tuns.md b/posts/tuns.md new file mode 100644 index 0000000..2447b00 --- /dev/null +++ b/posts/tuns.md @@ -0,0 +1,72 @@ +--- +title: tuns.sh +description: A guide to using tuns.sh +keywords: [pico, tuns] +--- + +Use your pico account to setup tunnels over ssh using +[sish](https://github.com/antoniomika/sish). + +In hopes of making premium services more worthwhile to users, we are testing a +new service called `tuns.sh`. `tuns.sh` provides HTTP(S)/TCP/TLS tunnels to +localhost using SSH. Once you have a pico account (as setup on our other +services) and we apply a feature flag to your user, you'll be able to login to +`tuns.sh`. + +## Closed beta + +Anyone can get an invite! + +The only requirement is to stay in our IRC channel and be willing to provide +feedback on how we can improve the service. + +Join our IRC channel +[#pico.sh @ libera.chat](https://web.libera.chat/gamja?autojoin=#pico.sh) and +ask for an invite. + +## Features + +- Tunnels to localhost using SSH +- Create a private connection from a remote service to your localhost + +## HTTP(S) Tunnels + +```bash +$ ssh -p 2222 -R 80:httpbin.org:80 tuns.sh +Press Ctrl-C to close the session. + +The subdomain localhost.tuns.sh is unavailable. Assigning a random subdomain. +Starting SSH Forwarding service for http:80. Forwarded connections can be accessed via the following methods: +Service console can be accessed here: https://flb.tuns.sh/_sish/console?x-authorization=[REDACTED] +HTTP: http://flb.tuns.sh +HTTPS: https://flb.tuns.sh +``` + +I can then access `http(s)://flb.tuns.sh` which will forward http requests to +`httpbin.org:80`. If I'm running a local webserver (like +`python3 -m http.server 8080`), I can replace `httpbin.org:80` with +`localhost:8080` and that address will forward to the http server I've just +started. + +HTTP(S) tunnels also support +[custom domains](https://github.com/antoniomika/sish#custom-domains). + +## TCP Tunnels + +```bash +$ ssh -p 2222 -R 10001:httpbin.org:80 tuns.sh +Press Ctrl-C to close the session. + +Starting SSH Forwarding service for tcp:10001. Forwarded connections can be accessed via the following methods: +TCP: tuns.sh:10001 +``` + +Which will allow me to access http://tuns.sh:10001 (or any other tcp service, +`httpbin.org:80` just happens to be an HTTP server) + +## And so much more + +That's just the beginning of what tuns.sh can do. Under the hood we're using a +project +[Antonio's been working on for a few years](https://github.com/antoniomika/sish). +There's a lot we can do here! diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/public/.gitkeep @@ -0,0 +1 @@ + diff --git a/static/docs.js b/static/docs.js new file mode 100644 index 0000000..56c1fd5 --- /dev/null +++ b/static/docs.js @@ -0,0 +1,77 @@ +document.addEventListener("DOMContentLoaded", init); + +function topNav() { + const topLevel = document.querySelectorAll(".nav-list li"); + topLevel.forEach((li) => { + const link = li.querySelector("a"); + const href = link.getAttribute("href"); + if (location.pathname === href) { + li.classList.add("current-page"); + } + }); +} + +function init() { + topNav(); + + let throttle = null; + const headers = document.querySelectorAll("h2,h3,h4"); + const scrollEl = document.querySelector(".content"); + scrollEl.addEventListener("scroll", updateNav, { passive: true }); + window.addEventListener("load", updateNav); + + const nav = document.querySelector(".toc"); + const btns = document.querySelectorAll(".toc-btn"); + btns.forEach((btn) => { + btn.addEventListener("click", (e) => { + e.preventDefault(); + nav.classList.toggle("nav-show"); + }); + }); + + function updateNav() { + let h2, h3, h4; + for (let i = 0; i < headers.length; i++) { + const h = headers[i]; + const top = h.getBoundingClientRect().top; + if (top > 10) { + break; + } + + if (h.tagName === "H2") { + h2 = h; + h3 = h4 = null; + } else if (h.tagName === "H3") { + h3 = h; + h4 = null; + } else { + h4 = h; + } + } + + for (let i = 0; i < headers.length; i++) { + const h = headers[i]; + if (h.tagName === "H4") { + continue; + } + const nav = document.getElementById("nav-" + h.id); + if (nav) { + nav.classList.toggle("current", h === (h3 || h2)); + } + } + + // Throttle to avoid crashes in Safari + const h = h4 || h3 || h2; + pathhash = location.pathname + (h ? "#" + h.id : ""); + if (throttle === null) { + throttle = setTimeout(updatePathname, 300); + } + } + + function updatePathname() { + throttle = null; + if (location.pathname + location.hash !== pathhash) { + history.replaceState(null, "", pathhash); + } + } +} diff --git a/static/favicon-16x16.png b/static/favicon-16x16.png new file mode 100644 index 0000000..566e73f Binary files /dev/null and b/static/favicon-16x16.png differ diff --git a/static/favicon-32x32.png b/static/favicon-32x32.png new file mode 100644 index 0000000..c5235fd Binary files /dev/null and b/static/favicon-32x32.png differ diff --git a/static/index.css b/static/index.css new file mode 100644 index 0000000..82aeae7 --- /dev/null +++ b/static/index.css @@ -0,0 +1,668 @@ +*, +::before, +::after { + box-sizing: border-box; +} + +::-moz-focus-inner { + border-style: none; + padding: 0; +} +:-moz-focusring { + outline: 1px dotted ButtonText; +} +:-moz-ui-invalid { + box-shadow: none; +} + +@media (prefers-color-scheme: light) { + :root { + --main-hue: 250; + --white: #6a737d; + --code: #fff8d3; + --code-border: #f0d547; + --pre: #f6f8fa; + --bg-color: #fff; + --text-color: #24292f; + --link-color: #005cc5; + --visited: #6f42c1; + --blockquote: #005cc5; + --blockquote-bg: #fff; + --hover: #d73a49; + --grey: #ccc; + --grey-light: #6a708e; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --main-hue: 250; + --white: #f2f2f2; + --code: #414558; + --code-border: #252525; + --pre: #252525; + --bg-color: #282a36; + --text-color: #f2f2f2; + --link-color: #8be9fd; + --visited: #bd93f9; + --blockquote: #bd93f9; + --blockquote-bg: #414558; + --hover: #ff80bf; + --grey: #414558; + --grey-light: #6a708e; + } +} + +html { + background-color: var(--bg-color); + color: var(--text-color); + font-size: 18px; + line-height: 1.5; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, + Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, + sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + -webkit-text-size-adjust: 100%; + -moz-tab-size: 4; + tab-size: 4; +} + +body { + margin: 0 auto; +} + +img { + max-width: 100%; + height: auto; +} + +b, +strong { + font-weight: bold; +} + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, + monospace; +} + +code, +kbd, +samp { + background-color: var(--code); + border: 1px solid var(--code-border); +} + +pre > code { + background-color: inherit; + padding: 0; + border: none; +} + +code { + border-radius: 0.3rem; + padding: 0.15rem 0.2rem 0.05rem; +} + +pre { + border-radius: 5px; + padding: 1rem; + margin: 1rem 0; + overflow-x: auto; + background-color: var(--pre) !important; +} + +small { + font-size: 0.8rem; +} + +summary { + display: list-item; +} + +h1, +h2, +h3 { + margin: 0; + padding: 0.6rem 0 0 0; + border: 0; + font-style: normal; + font-weight: inherit; + font-size: inherit; +} + +h1 > code { + font-size: inherit; +} + +h2 > code { + font-size: inherit; +} + +h3 > code { + font-size: inherit; +} + +path { + fill: var(--text-color); + stroke: var(--text-color); +} + +.md h1, +.md h2, +.md h3, +.md h4 { + margin: 1.5rem 0; + font-weight: bold; + border-bottom: 2px solid var(--grey); + padding-bottom: 15px; +} + +.md h1 a, +.md h2 a, +.md h3 a, +.md h4 a { + color: var(--grey); +} + +.md h1 { + font-size: 1.60rem; + line-height: 1.15; +} + +.md h2 { + font-size: 1.45rem; + line-height: 1.15; +} + +.md h3 { + font-size: 1.20rem; +} + +.md h4 { + font-size: 1rem; +} + +hr { + color: inherit; + border: 0; + margin: 0; + height: 1px; + background: var(--grey); + margin: 1rem auto; + text-align: center; +} + +a { + text-decoration: underline; + color: var(--link-color); +} + +a:hover, +a:visited:hover { + color: var(--hover); +} + +a:visited { + color: var(--visited); +} + +a.link-grey { + text-decoration: underline; + color: var(--white); +} + +a.link-grey:visited { + color: var(--white); +} + +section { + margin-bottom: 1.4rem; +} + +section:last-child { + margin-bottom: 0; +} + +header { + margin: 1rem auto; +} + +p { + margin: 0.8rem 0; +} + +article { + overflow-wrap: break-word; +} + +blockquote { + border-left: 5px solid var(--blockquote); + background-color: var(--blockquote-bg); + padding: 0.8rem; + margin: 1rem 0; +} + +blockquote > p { + margin: 0; +} + +ul, +ol { + padding: 0 0 0 2rem; + list-style-position: outside; +} + +ul[style*="list-style-type: none;"] { + padding: 0; +} + +li { + margin: 0.5rem 0; +} + +li > pre { + padding: 0; +} + +footer { + text-align: center; + margin-bottom: 4rem; +} + +dt { + font-weight: bold; +} + +dd { + margin-left: 0; +} + +dd:not(:last-child) { + margin-bottom: 0.5rem; +} + +figure { + margin: 0; +} + +.container { + max-width: 900px; +} + +.text-grey { + color: var(--grey); +} + +.text-3xl { + font-size: 3rem; +} + +.text-2xl { + font-size: 1.85rem; + line-height: 1.15; +} + +.text-xl { + font-size: 1.55rem; + line-height: 1.15; +} + +.text-lg { + font-size: 1.35rem; + line-height: 1.15; +} + +.text-md { + font-size: 1.15rem; + line-height: 1.15; +} + +.text-sm { + font-size: 0.875rem; +} + +.text-center { + text-align: center; +} + +.text-underline { + border-bottom: 3px solid var(--text-color); + padding-bottom: 3px; +} + +.font-bold { + font-weight: bold; +} + +.font-italic { + font-style: italic; +} + +.inline { + display: inline; +} + +.inline-block { + display: inline-block; +} + +.flex { + display: flex; +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.m-0 { + margin: 0; +} + +.mt { + margin-top: 0.5rem; +} + +.mt-4 { + margin-top: 1.5rem; +} + +.mb { + margin-bottom: 0.5rem; +} + +.mr { + margin-right: 0.5rem; +} + +.ml-sm { + margin-left: 0.25rem; +} + +.ml { + margin-left: 0.5rem; +} + +.my { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.my-2 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.my-4 { + margin-top: 2rem; + margin-bottom: 2rem; +} + +.mx { + margin-left: 0.5rem; + margin-right: 0.5rem; +} + +.mx-2 { + margin-left: 1rem; + margin-right: 1rem; +} + +.p-0 { + padding: 0; +} + +.justify-between { + justify-content: space-between; +} + +.justify-center { + justify-content: center; +} + +.gap { + gap: 1rem; +} + +.flex-1 { + flex: 1; +} + +.hero { + padding: 10rem 0; +} + +.mk-nav { + padding: 1rem; +} + +.mk-nav a, +.mk-nav a:visited, +.mk-nav a:visited:hover, +.mk-nav a:hover { + color: var(--white); + text-decoration: none; +} + +.mk-nav a:visited:hover, +.mk-nav a:hover { + text-decoration: underline; +} + +.mk-header { + line-height: 1; + display: inline-block; + background-image: linear-gradient(to right, #FF5555, #FF79C6, #F8F859); + color: transparent; + background-clip: text; + border: 3px solid #FF79C6; + padding: 8px 10px 10px 10px; + border-radius: 10px; + box-shadow: 0px 2px 4px 0px black; +} + +.mk-subheader { + font-size: 2rem; + line-height: 1; + border-bottom: none; + margin: 10px 0; + padding: 0; +} + +.mk-desc { + margin: 0; +} + +.features { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(225px, 1fr)); + gap: 1rem; +} + +.features > div { + border-radius: 10px; + border: 1px solid var(--white); + padding: 1rem; +} + +.features h2 { + border: none; + padding: 0; +} + +.btn-link { + border: 3px solid hsl(var(--main-hue), 92%, 66%); + background-color: hsl(var(--main-hue), 92%, 66%); + padding: 0.5rem 1rem; + border-radius: 0.25rem; + box-shadow: 0px 1px 2px 0px black; + color: var(--white); + text-decoration: none; + font-weight: bold; +} + +.btn-link:visited, +.btn-link:visited:hover, +.btn-link:hover { + color: var(--white); +} + +.docs { + height: 100vh; + overflow: hidden; + margin: 0; + display: grid; + grid-template-columns: 300px 1fr; + grid-template-rows: auto 1fr; +} + +.container-center { + width: 100%; + height: 100%; + display: flex; + justify-content: center; +} + +.content { + height: 100%; + overflow-y: auto; + padding-bottom: 300px; + padding-right: 1rem; + min-width: 600px; +} + +.toc { + padding: 0 1rem; + overflow-y: auto; +} + +.toc ul, ol { + list-style: none; + padding: 0 0 0 0.5rem; +} + +.toc li { + margin: 0 0 0.15rem 0; +} + +.toc li:first-child { + margin-top: 0; +} + +.toc a { + color: var(--grey-light); +} + +.toc-btn { + display: none; +} + +.current { + background-color: var(--blockquote-bg) !important; + border-right: 5px solid var(--blockquote); +} + +.current a { + color: var(--white); +} + +.current-page a { + color: var(--white); +} + + +.items-end { + align-items: end; +} + +.items-start { + align-items: start; +} + +.justify-end { + justify-content: end; +} + +.box { + border: 2px solid var(--grey-light); + border-radius: .5rem; + padding: 1rem; + text-decoration: none; + box-shadow: 0px 1px 2px 0px black; + color: var(--white); + width: 100%; + max-width: 200px; + height: 100px; +} + +.box:hover { + border: 2px solid var(--white); +} + +.box:visited, +.box:visited:hover, +.box:hover { + color: var(--white); +} + +.font-grey-light { + color: var(--grey-light); +} + +.hiw { + display: flex; + justify-content: center; +} + +.hiw img { + border: 1px solid var(--grey-light); + border-radius: 1rem; + margin: 0.5rem 0; + padding: 0.5rem; + width: 100%; +} + +.text-hdr { + color: var(--hover); +} + +.text-underline-hdr { + border-bottom: 3px solid var(--hover); + padding-bottom: 3px; +} + +@media only screen and (max-width: 600px) { + body { + padding: 0 1rem; + } + + header { + margin: 0; + } + + .toc { + width: 0px; + padding: 0; + } + + .nav-show { + width: 95%; + } + + .toc-btn { + display: block; + } + + .content { + } + + .docs { + grid-template-columns: auto 1fr; + } +} diff --git a/static/syntax.css b/static/syntax.css new file mode 100644 index 0000000..783ec66 --- /dev/null +++ b/static/syntax.css @@ -0,0 +1,700 @@ +@media (prefers-color-scheme: light) { + /* Background */ + .bg { + background-color: #ffffff; + } + /* PreWrapper */ + .chroma { + background-color: #ffffff; + } + /* Other */ + .chroma .x { + } + /* Error */ + .chroma .err { + background-color: #a848a8; + } + /* CodeLine */ + .chroma .cl { + } + /* LineTableTD */ + .chroma .lntd { + vertical-align: top; + padding: 0; + margin: 0; + border: 0; + } + /* LineTable */ + .chroma .lntable { + border-spacing: 0; + padding: 0; + margin: 0; + border: 0; + } + /* LineHighlight */ + .chroma .hl { + background-color: #ffffcc; + } + /* LineNumbersTable */ + .chroma .lnt { + white-space: pre; + user-select: none; + margin-right: 0.4em; + padding: 0 0.4em 0 0.4em; + color: #7f7f7f; + } + /* LineNumbers */ + .chroma .ln { + white-space: pre; + user-select: none; + margin-right: 0.4em; + padding: 0 0.4em 0 0.4em; + color: #7f7f7f; + } + /* Line */ + .chroma .line { + display: flex; + } + /* Keyword */ + .chroma .k { + color: #2838b0; + } + /* KeywordConstant */ + .chroma .kc { + color: #444444; + font-style: italic; + } + /* KeywordDeclaration */ + .chroma .kd { + color: #2838b0; + font-style: italic; + } + /* KeywordNamespace */ + .chroma .kn { + color: #2838b0; + } + /* KeywordPseudo */ + .chroma .kp { + color: #2838b0; + } + /* KeywordReserved */ + .chroma .kr { + color: #2838b0; + } + /* KeywordType */ + .chroma .kt { + color: #2838b0; + font-style: italic; + } + /* Name */ + .chroma .n { + } + /* NameAttribute */ + .chroma .na { + color: #388038; + } + /* NameBuiltin */ + .chroma .nb { + color: #388038; + } + /* NameBuiltinPseudo */ + .chroma .bp { + font-style: italic; + } + /* NameClass */ + .chroma .nc { + color: #287088; + } + /* NameConstant */ + .chroma .no { + color: #b85820; + } + /* NameDecorator */ + .chroma .nd { + color: #287088; + } + /* NameEntity */ + .chroma .ni { + color: #709030; + } + /* NameException */ + .chroma .ne { + color: #908828; + } + /* NameFunction */ + .chroma .nf { + color: #785840; + } + /* NameFunctionMagic */ + .chroma .fm { + color: #b85820; + } + /* NameLabel */ + .chroma .nl { + color: #289870; + } + /* NameNamespace */ + .chroma .nn { + color: #289870; + } + /* NameOther */ + .chroma .nx { + } + /* NameProperty */ + .chroma .py { + } + /* NameTag */ + .chroma .nt { + color: #2838b0; + } + /* NameVariable */ + .chroma .nv { + color: #b04040; + } + /* NameVariableClass */ + .chroma .vc { + } + /* NameVariableGlobal */ + .chroma .vg { + color: #908828; + } + /* NameVariableInstance */ + .chroma .vi { + } + /* NameVariableMagic */ + .chroma .vm { + color: #b85820; + } + /* Literal */ + .chroma .l { + } + /* LiteralDate */ + .chroma .ld { + } + /* LiteralString */ + .chroma .s { + color: #b83838; + } + /* LiteralStringAffix */ + .chroma .sa { + color: #444444; + } + /* LiteralStringBacktick */ + .chroma .sb { + color: #b83838; + } + /* LiteralStringChar */ + .chroma .sc { + color: #a848a8; + } + /* LiteralStringDelimiter */ + .chroma .dl { + color: #b85820; + } + /* LiteralStringDoc */ + .chroma .sd { + color: #b85820; + font-style: italic; + } + /* LiteralStringDouble */ + .chroma .s2 { + color: #b83838; + } + /* LiteralStringEscape */ + .chroma .se { + color: #709030; + } + /* LiteralStringHeredoc */ + .chroma .sh { + color: #b83838; + } + /* LiteralStringInterpol */ + .chroma .si { + color: #b83838; + text-decoration: underline; + } + /* LiteralStringOther */ + .chroma .sx { + color: #a848a8; + } + /* LiteralStringRegex */ + .chroma .sr { + color: #a848a8; + } + /* LiteralStringSingle */ + .chroma .s1 { + color: #b83838; + } + /* LiteralStringSymbol */ + .chroma .ss { + color: #b83838; + } + /* LiteralNumber */ + .chroma .m { + color: #444444; + } + /* LiteralNumberBin */ + .chroma .mb { + color: #444444; + } + /* LiteralNumberFloat */ + .chroma .mf { + color: #444444; + } + /* LiteralNumberHex */ + .chroma .mh { + color: #444444; + } + /* LiteralNumberInteger */ + .chroma .mi { + color: #444444; + } + /* LiteralNumberIntegerLong */ + .chroma .il { + color: #444444; + } + /* LiteralNumberOct */ + .chroma .mo { + color: #444444; + } + /* Operator */ + .chroma .o { + color: #666666; + } + /* OperatorWord */ + .chroma .ow { + color: #a848a8; + } + /* Punctuation */ + .chroma .p { + color: #888888; + } + /* Comment */ + .chroma .c { + color: #888888; + font-style: italic; + } + /* CommentHashbang */ + .chroma .ch { + color: #287088; + font-style: italic; + } + /* CommentMultiline */ + .chroma .cm { + color: #888888; + font-style: italic; + } + /* CommentSingle */ + .chroma .c1 { + color: #888888; + font-style: italic; + } + /* CommentSpecial */ + .chroma .cs { + color: #888888; + font-style: italic; + } + /* CommentPreproc */ + .chroma .cp { + color: #289870; + } + /* CommentPreprocFile */ + .chroma .cpf { + color: #289870; + } + /* Generic */ + .chroma .g { + } + /* GenericDeleted */ + .chroma .gd { + color: #c02828; + } + /* GenericEmph */ + .chroma .ge { + font-style: italic; + } + /* GenericError */ + .chroma .gr { + color: #c02828; + } + /* GenericHeading */ + .chroma .gh { + color: #666666; + } + /* GenericInserted */ + .chroma .gi { + color: #388038; + } + /* GenericOutput */ + .chroma .go { + color: #666666; + } + /* GenericPrompt */ + .chroma .gp { + color: #444444; + } + /* GenericStrong */ + .chroma .gs { + font-weight: bold; + } + /* GenericSubheading */ + .chroma .gu { + color: #444444; + } + /* GenericTraceback */ + .chroma .gt { + color: #2838b0; + } + /* GenericUnderline */ + .chroma .gl { + text-decoration: underline; + } + /* TextWhitespace */ + .chroma .w { + color: #a89028; + } +} + +@media (prefers-color-scheme: dark) { + /* Background */ + .bg { + color: #f8f8f2; + background-color: #282a36; + } + /* PreWrapper */ + .chroma { + color: #f8f8f2; + background-color: #282a36; + } + /* Other */ + .chroma .x { + } + /* Error */ + .chroma .err { + } + /* CodeLine */ + .chroma .cl { + } + /* LineTableTD */ + .chroma .lntd { + vertical-align: top; + padding: 0; + margin: 0; + border: 0; + } + /* LineTable */ + .chroma .lntable { + border-spacing: 0; + padding: 0; + margin: 0; + border: 0; + } + /* LineHighlight */ + .chroma .hl { + background-color: #ffffcc; + } + /* LineNumbersTable */ + .chroma .lnt { + white-space: pre; + user-select: none; + margin-right: 0.4em; + padding: 0 0.4em 0 0.4em; + color: #7f7f7f; + } + /* LineNumbers */ + .chroma .ln { + white-space: pre; + user-select: none; + margin-right: 0.4em; + padding: 0 0.4em 0 0.4em; + color: #7f7f7f; + } + /* Line */ + .chroma .line { + display: flex; + } + /* Keyword */ + .chroma .k { + color: #ff79c6; + } + /* KeywordConstant */ + .chroma .kc { + color: #ff79c6; + } + /* KeywordDeclaration */ + .chroma .kd { + color: #8be9fd; + font-style: italic; + } + /* KeywordNamespace */ + .chroma .kn { + color: #ff79c6; + } + /* KeywordPseudo */ + .chroma .kp { + color: #ff79c6; + } + /* KeywordReserved */ + .chroma .kr { + color: #ff79c6; + } + /* KeywordType */ + .chroma .kt { + color: #8be9fd; + } + /* Name */ + .chroma .n { + } + /* NameAttribute */ + .chroma .na { + color: #50fa7b; + } + /* NameBuiltin */ + .chroma .nb { + color: #8be9fd; + font-style: italic; + } + /* NameBuiltinPseudo */ + .chroma .bp { + } + /* NameClass */ + .chroma .nc { + color: #50fa7b; + } + /* NameConstant */ + .chroma .no { + } + /* NameDecorator */ + .chroma .nd { + } + /* NameEntity */ + .chroma .ni { + } + /* NameException */ + .chroma .ne { + } + /* NameFunction */ + .chroma .nf { + color: #50fa7b; + } + /* NameFunctionMagic */ + .chroma .fm { + } + /* NameLabel */ + .chroma .nl { + color: #8be9fd; + font-style: italic; + } + /* NameNamespace */ + .chroma .nn { + } + /* NameOther */ + .chroma .nx { + } + /* NameProperty */ + .chroma .py { + } + /* NameTag */ + .chroma .nt { + color: #ff79c6; + } + /* NameVariable */ + .chroma .nv { + color: #8be9fd; + font-style: italic; + } + /* NameVariableClass */ + .chroma .vc { + color: #8be9fd; + font-style: italic; + } + /* NameVariableGlobal */ + .chroma .vg { + color: #8be9fd; + font-style: italic; + } + /* NameVariableInstance */ + .chroma .vi { + color: #8be9fd; + font-style: italic; + } + /* NameVariableMagic */ + .chroma .vm { + } + /* Literal */ + .chroma .l { + } + /* LiteralDate */ + .chroma .ld { + } + /* LiteralString */ + .chroma .s { + color: #f1fa8c; + } + /* LiteralStringAffix */ + .chroma .sa { + color: #f1fa8c; + } + /* LiteralStringBacktick */ + .chroma .sb { + color: #f1fa8c; + } + /* LiteralStringChar */ + .chroma .sc { + color: #f1fa8c; + } + /* LiteralStringDelimiter */ + .chroma .dl { + color: #f1fa8c; + } + /* LiteralStringDoc */ + .chroma .sd { + color: #f1fa8c; + } + /* LiteralStringDouble */ + .chroma .s2 { + color: #f1fa8c; + } + /* LiteralStringEscape */ + .chroma .se { + color: #f1fa8c; + } + /* LiteralStringHeredoc */ + .chroma .sh { + color: #f1fa8c; + } + /* LiteralStringInterpol */ + .chroma .si { + color: #f1fa8c; + } + /* LiteralStringOther */ + .chroma .sx { + color: #f1fa8c; + } + /* LiteralStringRegex */ + .chroma .sr { + color: #f1fa8c; + } + /* LiteralStringSingle */ + .chroma .s1 { + color: #f1fa8c; + } + /* LiteralStringSymbol */ + .chroma .ss { + color: #f1fa8c; + } + /* LiteralNumber */ + .chroma .m { + color: #bd93f9; + } + /* LiteralNumberBin */ + .chroma .mb { + color: #bd93f9; + } + /* LiteralNumberFloat */ + .chroma .mf { + color: #bd93f9; + } + /* LiteralNumberHex */ + .chroma .mh { + color: #bd93f9; + } + /* LiteralNumberInteger */ + .chroma .mi { + color: #bd93f9; + } + /* LiteralNumberIntegerLong */ + .chroma .il { + color: #bd93f9; + } + /* LiteralNumberOct */ + .chroma .mo { + color: #bd93f9; + } + /* Operator */ + .chroma .o { + color: #ff79c6; + } + /* OperatorWord */ + .chroma .ow { + color: #ff79c6; + } + /* Punctuation */ + .chroma .p { + } + /* Comment */ + .chroma .c { + color: #6272a4; + } + /* CommentHashbang */ + .chroma .ch { + color: #6272a4; + } + /* CommentMultiline */ + .chroma .cm { + color: #6272a4; + } + /* CommentSingle */ + .chroma .c1 { + color: #6272a4; + } + /* CommentSpecial */ + .chroma .cs { + color: #6272a4; + } + /* CommentPreproc */ + .chroma .cp { + color: #ff79c6; + } + /* CommentPreprocFile */ + .chroma .cpf { + color: #ff79c6; + } + /* Generic */ + .chroma .g { + } + /* GenericDeleted */ + .chroma .gd { + color: #ff5555; + } + /* GenericEmph */ + .chroma .ge { + text-decoration: underline; + } + /* GenericError */ + .chroma .gr { + } + /* GenericHeading */ + .chroma .gh { + font-weight: bold; + } + /* GenericInserted */ + .chroma .gi { + color: #50fa7b; + font-weight: bold; + } + /* GenericOutput */ + .chroma .go { + color: #44475a; + } + /* GenericPrompt */ + .chroma .gp { + } + /* GenericStrong */ + .chroma .gs { + } + /* GenericSubheading */ + .chroma .gu { + font-weight: bold; + } + /* GenericTraceback */ + .chroma .gt { + } + /* GenericUnderline */ + .chroma .gl { + text-decoration: underline; + } + /* TextWhitespace */ + .chroma .w { + } +} diff --git a/tmpl/base.layout.tmpl b/tmpl/base.layout.tmpl new file mode 100644 index 0000000..8e2fabc --- /dev/null +++ b/tmpl/base.layout.tmpl @@ -0,0 +1,20 @@ +{{define "base"}} + + + + {{template "title" .}} + + + + {{template "meta" .}} + + + + + + + + {{template "body" .}} + + +{{end}} diff --git a/tmpl/footer.partial.tmpl b/tmpl/footer.partial.tmpl new file mode 100644 index 0000000..a66ff5a --- /dev/null +++ b/tmpl/footer.partial.tmpl @@ -0,0 +1,5 @@ +{{define "footer"}} +
+ Made in Ann Arbor +
+{{end}} diff --git a/tmpl/marketing.page.tmpl b/tmpl/marketing.page.tmpl new file mode 100644 index 0000000..a24e92f --- /dev/null +++ b/tmpl/marketing.page.tmpl @@ -0,0 +1,100 @@ +{{template "base" .}} + +{{define "title"}}{{.Data.Title}}{{end}} + +{{define "meta"}} +{{end}} + +{{define "attrs"}}class="container"{{end}} + +{{define "body"}} + + +
+
+
+
+

pico

+

Providing tools and services to improve communication and collaboration on the web

+ GET STARTED +
+
+ +
+
+

+ pgs.sh +

+

A zero-dependency static site hosting service for hackers

+
+ +
+

+ prose.sh +

+

A blog platform for hackers

+
+ +
+

+ pastes.sh +

+

A pastebin for hackers

+
+ +
+

+ imgs.sh +

+

Image hosting for hackers

+
+ +
+

+ feeds.sh +

+

An rss email notification service

+
+ +
+

+ lists.sh +

+

A microblog for lists

+
+ +
+

+ tuns.sh +

+

Access localhost using https

+
+
+
+ +
+ + {{template "footer" .}} +
+{{end}} diff --git a/tmpl/post.page.tmpl b/tmpl/post.page.tmpl new file mode 100644 index 0000000..2b349d1 --- /dev/null +++ b/tmpl/post.page.tmpl @@ -0,0 +1,55 @@ +{{template "base" .}} + +{{define "title"}}{{.Data.Title}}{{end}} + +{{define "meta"}} + +{{end}} + +{{define "attrs"}}class="container-center"{{end}} + +{{define "body"}} +
+ + +
+
+ + +

{{.Data.Title}}

+

{{.Data.Description}}

+ +
+ +
+ {{.Data.Html}} +
+ +
+ {{if .Prev}} + +
+
<< PREVIOUS
+
{{.Prev.Text}}
+
+
+ {{end}} + + {{if .Next}} + +
+
+ NEXT >> +
+
{{.Next.Text}}
+
+
+ {{end}} +
+
+
+
+{{end}} diff --git a/tmpl/toc.partial.tmpl b/tmpl/toc.partial.tmpl new file mode 100644 index 0000000..3710b95 --- /dev/null +++ b/tmpl/toc.partial.tmpl @@ -0,0 +1,28 @@ +{{define "toc"}} + + +

pico

+ + + + + + + + +{{end}}