├── .dockerignore ├── .editorconfig ├── .github └── workflows │ └── fly.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── extensions.json └── settings.json ├── Caddyfile ├── Dockerfile ├── LICENSE ├── README.md ├── astro.config.ts ├── eslint.config.ts ├── flake.lock ├── flake.nix ├── fly.toml ├── giscus.json ├── lib.d.ts ├── package.json ├── pnpm-lock.yaml ├── public ├── .well-known │ └── security.txt ├── avatar.webp ├── buttons │ └── friends │ │ ├── adryd.png │ │ ├── alula.png │ │ ├── alyxia.png │ │ ├── auravoid.png │ │ ├── cin.png │ │ ├── cynthia.png │ │ ├── erin.png │ │ ├── espi.png │ │ ├── fleepy.png │ │ ├── lewisakura.png │ │ ├── maia.png │ │ ├── ovyerus.png │ │ ├── pihnea.png │ │ ├── resolv.png │ │ ├── sapphic.png │ │ ├── spotlight.gif │ │ ├── utsuhorocks.png │ │ ├── ven.png │ │ ├── vin.png │ │ ├── ziad87.gif │ │ └── zptr.gif ├── domains.txt ├── favicon.ico ├── favicon.png ├── keys.txt └── rss │ ├── feed.xsl │ └── style.css ├── src ├── assets │ └── posts │ │ └── oneshot-on-linux │ │ ├── compatibility-tab.png │ │ ├── oneshotsaves.png │ │ ├── protondocuments.png │ │ └── spoiler-clover-program.png ├── components │ ├── base │ │ ├── Avatar.astro │ │ ├── Footer.astro │ │ ├── Head.astro │ │ ├── HeadMeta.astro │ │ ├── NavBar.astro │ │ └── NavLink.astro │ ├── blog │ │ ├── Card.astro │ │ ├── Comments.astro │ │ ├── OGImage.astro │ │ ├── Pagination.astro │ │ ├── RSS.astro │ │ └── Tag.astro │ ├── home │ │ ├── Donation.astro │ │ └── Friends.astro │ ├── markdown │ │ ├── Image.astro │ │ └── Spoiler.astro │ └── react │ │ ├── ZoomContent.tsx │ │ └── ZoomWrapper.tsx ├── content.config.ts ├── content │ ├── articles │ │ ├── oneshot-on-linux.mdx │ │ └── using-cloudflare-r2-on-owncloud.mdx │ └── info │ │ ├── about.mdx │ │ └── contact.mdx ├── layouts │ ├── Article.astro │ ├── ArticleList.astro │ ├── Markdown.astro │ └── Page.astro ├── pages │ ├── 404.astro │ ├── [...id].astro │ ├── article │ │ ├── [...id].astro │ │ └── [...id].png.ts │ ├── articles │ │ ├── [...page].astro │ │ └── tags │ │ │ └── [tag] │ │ │ └── [...page].astro │ ├── index.astro │ ├── robots.txt.ts │ └── rss.xml.ts ├── site.config.ts ├── styles │ └── article.css ├── typings │ ├── config.type.ts │ └── utils.type.ts └── utils │ ├── article.ts │ ├── friends.ts │ └── image.ts ├── tailwind.config.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # astro 5 | .astro 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # environment variables 11 | **/.env.example 12 | 13 | # editors / ides 14 | .idea 15 | .vscode 16 | 17 | # git 18 | .git 19 | 20 | # nix 21 | **/flake.nix 22 | **/flake.lock 23 | 24 | # toolchains 25 | **/.editorconfig 26 | **/.prettierignore 27 | **/.prettierrc.json 28 | **/eslint.config.ts -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = false 7 | trim_trailing_whitespace = true 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [{*.md,*.mdx}] 13 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/workflows/fly.yml: -------------------------------------------------------------------------------- 1 | # See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/ 2 | 3 | name: Fly Deploy 4 | on: 5 | push: 6 | branches: 7 | - master 8 | jobs: 9 | deploy: 10 | name: Deploy app 11 | runs-on: ubuntu-latest 12 | concurrency: deploy-group # optional: ensure only one action runs at a time 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: superfly/flyctl-actions/setup-flyctl@master 16 | - run: flyctl deploy --remote-only 17 | env: 18 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | .output/ 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | logs 11 | npm-debug.log* 12 | 13 | # environment variables 14 | .env 15 | 16 | # IntelliJ IDEA / WebStorm 17 | .idea 18 | 19 | # OS-specific files 20 | .DS_Store 21 | desktop.ini -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | .output/ 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | 14 | # environment variables 15 | .env 16 | .env.production 17 | 18 | # lock 19 | package-lock.json 20 | pnpm-lock.yaml 21 | 22 | *.md 23 | *.mdx -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"], 3 | "overrides": [ 4 | { 5 | "files": "*.astro", 6 | "options": { 7 | "parser": "astro" 8 | } 9 | } 10 | ], 11 | "proseWrap": "always", 12 | "printWidth": 100, 13 | "quoteProps": "preserve", 14 | "semi": true, 15 | "singleQuote": true, 16 | "tabWidth": 2, 17 | "trailingComma": "es5" 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "astro-build.astro-vscode", 4 | "matthewpi.caddyfile-support", 5 | "editorconfig.editorconfig", 6 | "dbaeumer.vscode-eslint", 7 | "unifiedjs.vscode-mdx", 8 | "esbenp.prettier-vscode", 9 | "bradlc.vscode-tailwindcss" 10 | ], 11 | "unwantedRecommendations": [] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true, 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit" 6 | }, 7 | "files.associations": { 8 | "*.mdx": "markdown" 9 | }, 10 | "eslint.validate": ["javascript", "astro", "typescript"], 11 | "prettier.documentSelectors": ["**/*.astro"], 12 | "[css]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode" 14 | }, 15 | "[javascript]": { 16 | "editor.defaultFormatter": "esbenp.prettier-vscode" 17 | }, 18 | "[typescript]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode" 20 | }, 21 | "[json]": { 22 | "editor.defaultFormatter": "esbenp.prettier-vscode" 23 | }, 24 | "[jsonc]": { 25 | "editor.defaultFormatter": "esbenp.prettier-vscode" 26 | }, 27 | "[markdown]": { 28 | "editor.defaultFormatter": "esbenp.prettier-vscode" 29 | }, 30 | "[astro]": { 31 | "editor.defaultFormatter": "esbenp.prettier-vscode" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | # Credit goes to Ovyerus for the Caddyfile :) 2 | 3 | # HTTPS is expected to be handled by Cloudflare or some other reverse proxy. 4 | { 5 | auto_https off 6 | 7 | # see: https://caddyserver.com/docs/caddyfile/directives#directive-order 8 | order umami after route 9 | } 10 | 11 | :8080 { 12 | @localhost { 13 | host localhost 14 | } 15 | 16 | root @localhost dist 17 | root * /var/www/html 18 | encode gzip # TODO: check options 19 | file_server 20 | log 21 | 22 | umami { 23 | event_endpoint "https://umami.sappho.systems/api/send" 24 | website_uuid "1279cae7-78b6-4bfd-b13c-10d4dd9893ee" 25 | allowed_extensions "" .html .xml 26 | device_detection 27 | } 28 | 29 | redir /.well-known/webfinger https://auth.sappho.systems/.well-known/webfinger 30 | 31 | handle_errors { 32 | @404 { 33 | expression {http.error.status_code} == 404 34 | } 35 | 36 | rewrite @404 /404.html 37 | file_server 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1 2 | 3 | # Base image with pnpm setup 4 | FROM node:20-alpine AS base 5 | ENV PNPM_HOME="/pnpm" 6 | ENV PATH="$PNPM_HOME:$PATH" 7 | 8 | # Enable corepack to use pnpm 9 | RUN npm install -g corepack@latest 10 | RUN corepack enable 11 | ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 12 | 13 | # Set working directory and copy files 14 | WORKDIR /app 15 | COPY . . 16 | 17 | # Build stage 18 | FROM base AS build 19 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --prod 20 | ENV ASTRO_TELEMETRY_DISABLED=1 21 | RUN pnpm run build 22 | 23 | # Caddy builder stage for Cloudflare DNS 24 | FROM caddy:builder-alpine AS caddy-builder 25 | 26 | RUN xcaddy build \ 27 | --with github.com/caddy-dns/cloudflare \ 28 | --with github.com/jonaharagon/caddy-umami 29 | 30 | # Final stage using Caddy 31 | FROM caddy:alpine 32 | 33 | # Copy the built Caddy with Cloudflare DNS support 34 | COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy 35 | 36 | # Copy built static files to Caddy's serving directory 37 | COPY --from=build /app/dist /var/www/html 38 | COPY Caddyfile /Caddyfile 39 | 40 | # Expose port and start Caddy 41 | EXPOSE 8080 42 | CMD ["caddy", "run", "--config", "/Caddyfile"] 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | zlib License 2 | 3 | (C) 2024 Sapphic Angels 4 | 5 | This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable 6 | for any damages arising from the use of this software. 7 | 8 | Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it 9 | and redistribute it freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If 12 | you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not 13 | required. 14 | 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original 15 | software. 16 | 3. This notice may not be removed or altered from any source distribution. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # 🌸 [sapphic.moe](sapphic.moe) 3 | The website for the Sapphic Angels system. 4 | 5 | Powered by [Astro][astro] and [Tailwind CSS][tailwind]. 6 | 7 | [astro]: https://astro.build 'Astro' 8 | [tailwind]: https://tailwindcss.com 'Tailwind CSS' 9 | 10 | ## License 11 | 12 | The source code for this website is licensed under the [zlib][license] license. 13 | 14 | Contents in the `/src/content/articles` and `/src/content/info` folders are licensed under the [Creative Commons CC-BY-NC-SA 4.0 International License][cc-license] license. 15 | 16 | © 2024 Sapphic Angels. 17 | 18 | [cc-license]: https://creativecommons.org/licenses/by-nc-sa/4.0/ 'Creative Commons CC-BY-NC-SA 4.0 International License' 19 | [license]: https://github.com/solelychloe/sapphic.moe/blob/main/LICENSE 'zlib License' 20 | -------------------------------------------------------------------------------- /astro.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import { base } from './src/site.config'; 3 | 4 | // Official Astro integrations 5 | import mdx from '@astrojs/mdx'; 6 | import react from '@astrojs/react'; 7 | import sitemap from '@astrojs/sitemap'; 8 | import tailwind from '@astrojs/tailwind'; 9 | 10 | // Third-party Astro integrations 11 | import autoImport from 'astro-auto-import'; 12 | import expressiveCode from 'astro-expressive-code'; 13 | import icon from 'astro-icon'; 14 | 15 | // Rehype and Remark plugins 16 | import a11yEmoji from '@fec/remark-a11y-emoji'; 17 | import figureCaption from '@microflash/remark-figure-caption'; 18 | import autolinkHeadings from 'rehype-autolink-headings'; 19 | import externalLinks from 'rehype-external-links'; 20 | import readingTime from './src/utils/article'; 21 | import slug from 'rehype-slug'; 22 | import tableOfContents from 'remark-toc'; 23 | 24 | // Vite 25 | import arrayBuffer from 'vite-plugin-arraybuffer'; 26 | 27 | // Expressive Code theme 28 | import catppuccinMocha from '@catppuccin/vscode/themes/mocha.json'; 29 | 30 | // https://astro.build/config 31 | export default defineConfig({ 32 | site: base.site.url, 33 | // redirects: { 34 | // '/.well-known/webfinger': 'https://auth.sappho.systems/.well-known/webfinger', 35 | // }, 36 | integrations: [ 37 | tailwind(), 38 | autoImport({ 39 | imports: ['$components/markdown/Image.astro', '$components/markdown/Spoiler.astro'], 40 | }), 41 | expressiveCode({ 42 | themes: [catppuccinMocha], 43 | styleOverrides: { 44 | frames: { 45 | shadowColor: '#000', 46 | editorActiveTabIndicatorTopColor: catppuccinMocha.colors['terminal.ansiMagenta'], 47 | editorActiveTabForeground: catppuccinMocha.colors['terminal.ansiMagenta'], 48 | }, 49 | }, 50 | }), 51 | mdx(), 52 | icon({ 53 | include: { 54 | mdi: ['*'], 55 | }, 56 | }), 57 | sitemap(), 58 | react(), 59 | ], 60 | markdown: { 61 | remarkPlugins: [ 62 | a11yEmoji, 63 | figureCaption, 64 | readingTime, 65 | [ 66 | tableOfContents, 67 | { 68 | tight: true, 69 | }, 70 | ], 71 | ], 72 | rehypePlugins: [ 73 | slug, 74 | [ 75 | autolinkHeadings, 76 | { 77 | behavior: 'append', 78 | content: { 79 | type: 'text', 80 | value: '#', 81 | }, 82 | }, 83 | ], 84 | [ 85 | externalLinks, 86 | { 87 | target: '_blank', 88 | contentProperties: { 89 | ariaHidden: true, 90 | className: ['external-link'], 91 | }, 92 | content: { 93 | type: 'text', 94 | value: ' ↗', 95 | }, 96 | }, 97 | ], 98 | ], 99 | }, 100 | vite: { 101 | plugins: [arrayBuffer()], 102 | }, 103 | }); 104 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import tseslint from 'typescript-eslint'; 3 | import astroPlugin from 'eslint-plugin-astro'; 4 | 5 | export default tseslint.config( 6 | { 7 | ignores: ['**/node_modules', '.astro', '.output', '**/.env', '**/dist'], 8 | }, 9 | 10 | tseslint.configs.recommended, 11 | eslint.configs.recommended, 12 | tseslint.configs.eslintRecommended, 13 | ...astroPlugin.configs.recommended, 14 | ...astroPlugin.configs['jsx-a11y-recommended'], 15 | 16 | { 17 | files: ['**/*.astro'], 18 | languageOptions: { 19 | parserOptions: { 20 | // `projectService: true` does not work properly with Astro ruleset. 21 | // Using `project: true` instead. 22 | projectService: false, 23 | project: true, 24 | }, 25 | }, 26 | rules: { 27 | 'no-console': ['warn'], 28 | 'no-warning-comments': ['warn'], 29 | 30 | 'no-unused-vars': 'off', 31 | '@typescript-eslint/no-unused-vars': [ 32 | 'error', 33 | { 34 | args: 'all', 35 | argsIgnorePattern: '^_', 36 | caughtErrors: 'all', 37 | caughtErrorsIgnorePattern: '^_', 38 | destructuredArrayIgnorePattern: '^_', 39 | varsIgnorePattern: '^_', 40 | ignoreRestSiblings: true, 41 | }, 42 | ], 43 | 44 | 'astro/jsx-a11y/anchor-ambiguous-text': [ 45 | 'error', 46 | { 47 | words: [ 48 | // Defaults 49 | 'click here', 50 | 'here', 51 | 'link', 52 | 'a link', 53 | 'learn more', 54 | ], 55 | }, 56 | ], 57 | 58 | 'astro/semi': ['error', 'always'], 59 | }, 60 | }, 61 | { 62 | files: ['**/*.astro'], 63 | extends: [tseslint.configs.disableTypeChecked], 64 | } 65 | ); 66 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1737003892, 6 | "narHash": "sha256-RCzJE9wKByLCXmRBp+z8LK9EgdW+K+W/DXnJS4S/NVo=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "ae06b9c2d83cb5c8b12d7d0e32692e93d1379713", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "🌸 The website for the Sapphic Angels system. Made with Astro."; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 6 | }; 7 | 8 | outputs = 9 | { self, nixpkgs }@inputs: 10 | let 11 | forAllSystems = 12 | function: 13 | nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed ( 14 | system: function nixpkgs.legacyPackages.${system} 15 | ); 16 | in 17 | { 18 | devShells = forAllSystems (pkgs: { 19 | default = pkgs.mkShellNoCC { 20 | packages = with pkgs; [ 21 | nodejs 22 | pnpm 23 | flyctl 24 | caddy 25 | ]; 26 | }; 27 | }); 28 | }; 29 | } -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for sapphic on 2025-01-15T05:46:32+05:00 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = 'sapphic' 7 | primary_region = 'fra' 8 | 9 | [build] 10 | 11 | [http_service] 12 | internal_port = 8080 13 | force_https = true 14 | auto_stop_machines = true 15 | auto_start_machines = true 16 | min_machines_running = 0 17 | processes = ['app'] 18 | 19 | [[vm]] 20 | size = 'shared-cpu-1x' 21 | -------------------------------------------------------------------------------- /giscus.json: -------------------------------------------------------------------------------- 1 | { 2 | "origins": ["https://sapphic.moe", "https://sapphic.fly.dev"] 3 | } 4 | -------------------------------------------------------------------------------- /lib.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@fec/remark-a11y-emoji'; 2 | declare module '@microflash/remark-figure-caption'; 3 | declare module '@catppuccin/tailwindcss'; 4 | 5 | declare module '*.woff?arraybuffer' { 6 | const content: ArrayBuffer; 7 | export default content; 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sapphic/site", 3 | "version": "3.0.0", 4 | "type": "module", 5 | "author": "Sapphic Angels ", 6 | "license": "zlib", 7 | "private": true, 8 | "scripts": { 9 | "build": "astro build", 10 | "check": "astro check", 11 | "dev": "astro dev", 12 | "format": "prettier --w --plugin-search-dir=. --cache .", 13 | "lint": "eslint --fix src/**/*.{ts,astro}", 14 | "start": "astro preview", 15 | "serve": "caddy run --config Caddyfile", 16 | "sync": "astro sync", 17 | "test": "eslint .; prettier -c ." 18 | }, 19 | "repository": "https://github.com/SapphicMoe/sapphic.moe", 20 | "packageManager": "pnpm@9.15.4", 21 | "postcss": { 22 | "plugins": { 23 | "tailwindcss/nesting": {}, 24 | "tailwindcss": {}, 25 | "autoprefixer": {} 26 | } 27 | }, 28 | "dependencies": { 29 | "@astrojs/mdx": "^4.1.0", 30 | "@astrojs/react": "^4.2.1", 31 | "@astrojs/rss": "^4.0.11", 32 | "@astrojs/sitemap": "^3.2.1", 33 | "@astrojs/tailwind": "^5.1.5", 34 | "@catppuccin/tailwindcss": "^0.1.6", 35 | "@catppuccin/vscode": "^3.16.1", 36 | "@fec/remark-a11y-emoji": "^4.0.2", 37 | "@fontsource/atkinson-hyperlegible": "^5.2.5", 38 | "@fontsource/iosevka": "^5.2.5", 39 | "@iconify-json/mdi": "^1.2.3", 40 | "@microflash/remark-figure-caption": "^2.0.2", 41 | "@napi-rs/image": "^1.9.2", 42 | "@tailwindcss/typography": "^0.5.16", 43 | "astro": "^5.4.1", 44 | "astro-auto-import": "^0.4.4", 45 | "astro-expressive-code": "^0.40.2", 46 | "astro-icon": "^1.1.5", 47 | "astro-seo": "^0.8.4", 48 | "date-fns": "^4.1.0", 49 | "mdast-util-to-string": "^4.0.0", 50 | "react": "^19.0.0", 51 | "react-dom": "^19.0.0", 52 | "react-medium-image-zoom": "^5.2.14", 53 | "reading-time": "^1.5.0", 54 | "rehype-autolink-headings": "^7.1.0", 55 | "rehype-external-links": "^3.0.0", 56 | "rehype-slug": "^6.0.0", 57 | "remark-toc": "^9.0.0", 58 | "satori": "^0.12.1", 59 | "satori-html": "^0.3.2", 60 | "sharp": "^0.33.5", 61 | "tailwindcss": "^3.4.17", 62 | "ts-dedent": "^2.2.0", 63 | "ultrahtml": "^1.5.3", 64 | "vite-plugin-arraybuffer": "^0.1.0" 65 | }, 66 | "devDependencies": { 67 | "@eslint/js": "^9.21.0", 68 | "@types/eslint": "^9.6.1", 69 | "@types/react": "^19.0.10", 70 | "astro-eslint-parser": "^1.2.1", 71 | "astro-worker-links": "^0.3.1", 72 | "autoprefixer": "^10.4.20", 73 | "eslint": "^9.21.0", 74 | "eslint-plugin-astro": "^1.3.1", 75 | "eslint-plugin-jsx-a11y": "^6.10.2", 76 | "jiti": "^2.4.2", 77 | "postcss": "^8.5.3", 78 | "prettier": "^3.5.2", 79 | "prettier-plugin-astro": "^0.14.1", 80 | "prettier-plugin-tailwindcss": "^0.6.11", 81 | "typescript": "^5.8.2", 82 | "typescript-eslint": "^8.25.0" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /public/.well-known/security.txt: -------------------------------------------------------------------------------- 1 | Contact: mailto:solely@riseup.net 2 | Expires: 2025-01-18T00:00:00.000Z 3 | Encryption: https://sapphic.moe/ssh.txt 4 | Preferred-Languages: en,ru 5 | Canonical: https://sapphic.moe/.well-known/security.txt 6 | -------------------------------------------------------------------------------- /public/avatar.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/avatar.webp -------------------------------------------------------------------------------- /public/buttons/friends/adryd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/adryd.png -------------------------------------------------------------------------------- /public/buttons/friends/alula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/alula.png -------------------------------------------------------------------------------- /public/buttons/friends/alyxia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/alyxia.png -------------------------------------------------------------------------------- /public/buttons/friends/auravoid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/auravoid.png -------------------------------------------------------------------------------- /public/buttons/friends/cin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/cin.png -------------------------------------------------------------------------------- /public/buttons/friends/cynthia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/cynthia.png -------------------------------------------------------------------------------- /public/buttons/friends/erin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/erin.png -------------------------------------------------------------------------------- /public/buttons/friends/espi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/espi.png -------------------------------------------------------------------------------- /public/buttons/friends/fleepy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/fleepy.png -------------------------------------------------------------------------------- /public/buttons/friends/lewisakura.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/lewisakura.png -------------------------------------------------------------------------------- /public/buttons/friends/maia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/maia.png -------------------------------------------------------------------------------- /public/buttons/friends/ovyerus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/ovyerus.png -------------------------------------------------------------------------------- /public/buttons/friends/pihnea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/pihnea.png -------------------------------------------------------------------------------- /public/buttons/friends/resolv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/resolv.png -------------------------------------------------------------------------------- /public/buttons/friends/sapphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/sapphic.png -------------------------------------------------------------------------------- /public/buttons/friends/spotlight.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/spotlight.gif -------------------------------------------------------------------------------- /public/buttons/friends/utsuhorocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/utsuhorocks.png -------------------------------------------------------------------------------- /public/buttons/friends/ven.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/ven.png -------------------------------------------------------------------------------- /public/buttons/friends/vin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/vin.png -------------------------------------------------------------------------------- /public/buttons/friends/ziad87.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/ziad87.gif -------------------------------------------------------------------------------- /public/buttons/friends/zptr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/buttons/friends/zptr.gif -------------------------------------------------------------------------------- /public/domains.txt: -------------------------------------------------------------------------------- 1 | # All root domains owned (on a registry level) by Sapphic Angels. 2 | # Domains that redirect to domains not in this list may not be in complete control. 3 | # Content outside of these domains cannot be guarenteed to be owned or managed by Sapphic Angels. 4 | 5 | # Last updated: 2023-05-01T17:16:26.486Z 6 | 7 | # Our main site 8 | sapphic.moe 9 | 10 | # URL shortener 11 | solstice.tf 12 | 13 | # Parallel Program Report. (donated by Leaf, thank you!) 14 | parallel.report -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/public/favicon.png -------------------------------------------------------------------------------- /public/keys.txt: -------------------------------------------------------------------------------- 1 | # My public SSH keys. 2 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICM6XP+CNc2CStEDe/W4LfkcRcG98obQiM2aqnydCRbX chloe@solely 3 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJug+9rnFngnFQpY0lAO0NuVBhDCcJc5imPHazgOSTTx chloe@solely -------------------------------------------------------------------------------- /public/rss/feed.xsl: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | RSS feed for <xsl:value-of select="/rss/channel/title"/> 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 |
35 |
36 |
37 |

38 | 39 | 40 |

41 | 42 |

43 | 44 |

45 | 46 | 49 |
50 | 51 |
52 |

## Articles

53 | 54 | 55 | 84 | 85 | 86 | 115 |
116 |
117 |
118 | 119 | 120 |
121 |
122 | -------------------------------------------------------------------------------- /public/rss/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #1e1e2e; 3 | width: auto; 4 | padding: 8px 14px; 5 | color: #cdd6f4; 6 | font-family: 'Atkinson Hyperlegible', 'Segoe UI', apple-system, BlinkMacSystemFont, Futura, 7 | Roboto, Arial, system-ui, sans-serif; 8 | } 9 | 10 | .container { 11 | align-items: center; 12 | display: flex; 13 | justify-content: center; 14 | } 15 | 16 | .item { 17 | max-width: 768px; 18 | } 19 | 20 | .footer-links { 21 | display: flex; 22 | flex-direction: row; 23 | gap: 6px; 24 | } 25 | 26 | hr { 27 | border-color: #45475a; 28 | margin-top: 30px; 29 | } 30 | 31 | .title { 32 | display: flex; 33 | align-items: center; 34 | gap: 10px; 35 | margin-bottom: 0.5rem; 36 | } 37 | 38 | .description { 39 | font-size: 1.1rem; 40 | font-style: italic; 41 | } 42 | 43 | .banner { 44 | display: flex; 45 | flex-direction: column; 46 | gap: 1.25rem; 47 | border-radius: 0.375rem; 48 | border-width: 1px; 49 | padding: 1.25rem; 50 | } 51 | 52 | .banner-header { 53 | display: flex; 54 | flex-direction: column; 55 | row-gap: 0.5rem; 56 | } 57 | 58 | a { 59 | color: #f5c2e7; 60 | text-decoration: none; 61 | transition: all 0.2s ease-out; 62 | } 63 | 64 | a:hover { 65 | text-decoration: underline; 66 | opacity: 0.5; 67 | } 68 | -------------------------------------------------------------------------------- /src/assets/posts/oneshot-on-linux/compatibility-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/src/assets/posts/oneshot-on-linux/compatibility-tab.png -------------------------------------------------------------------------------- /src/assets/posts/oneshot-on-linux/oneshotsaves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/src/assets/posts/oneshot-on-linux/oneshotsaves.png -------------------------------------------------------------------------------- /src/assets/posts/oneshot-on-linux/protondocuments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/src/assets/posts/oneshot-on-linux/protondocuments.png -------------------------------------------------------------------------------- /src/assets/posts/oneshot-on-linux/spoiler-clover-program.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SapphoSys/sapphic.moe/348348fca5ef4006ea1fa58d5fc71bf4102e36f0/src/assets/posts/oneshot-on-linux/spoiler-clover-program.png -------------------------------------------------------------------------------- /src/components/base/Avatar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Image } from 'astro:assets'; 3 | 4 | interface Props { 5 | class?: string; 6 | width: number; 7 | height: number; 8 | } 9 | 10 | const { class: className, width, height } = Astro.props; 11 | --- 12 | 13 | A link to my avatar. The character displayed here is Silver Wolf from the videogame Honkai: Star Rail. 20 | -------------------------------------------------------------------------------- /src/components/base/Footer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from 'astro-icon/components'; 3 | --- 4 | 5 | 38 | -------------------------------------------------------------------------------- /src/components/base/Head.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { ClientRouter } from 'astro:transitions'; 3 | 4 | import HeadMeta from '$components/base/HeadMeta.astro'; 5 | 6 | interface Props { 7 | description?: string; 8 | pageTitle: string; 9 | title: string; 10 | created?: string; 11 | ogImage?: { 12 | src?: string; 13 | alt?: string; 14 | }; 15 | } 16 | 17 | const { description, pageTitle, title, created, ogImage } = Astro.props; 18 | --- 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/base/HeadMeta.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { SEO } from 'astro-seo'; 3 | import { base } from '$config'; 4 | 5 | interface Props { 6 | description?: string; 7 | pageTitle: string; 8 | title: string; 9 | created?: string; 10 | ogImage?: { 11 | src?: string; 12 | alt?: string; 13 | }; 14 | } 15 | 16 | const { description, pageTitle, ogImage, created, title } = Astro.props; 17 | 18 | import { blog } from '$config'; 19 | 20 | const siteName = Astro.site?.hostname; 21 | const isArticle = Astro.url.pathname.startsWith('/article/'); 22 | 23 | const IS_PRODUCTION = import.meta.env.NODE_ENV === 'production'; 24 | 25 | const generateOpenGraphImage = () => { 26 | return { 27 | src: IS_PRODUCTION 28 | ? new URL(ogImage?.src ?? base.images.favicon.fileName, Astro.site).href 29 | : (ogImage?.src ?? base.images.favicon.fileName), 30 | alt: isArticle ? ogImage?.alt : base.images.favicon.altText, 31 | }; 32 | }; 33 | 34 | const generateTwitterImage = (image?: string) => { 35 | return { 36 | src: IS_PRODUCTION 37 | ? new URL(image ?? base.images.twitter.fileName, Astro.site).href 38 | : (image ?? base.images.twitter.fileName), 39 | alt: isArticle ? ogImage?.alt : base.images.twitter.altText, 40 | }; 41 | }; 42 | --- 43 | 44 | 83 | 84 | {blog.rss.enabled && } 85 | -------------------------------------------------------------------------------- /src/components/base/NavBar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Avatar from '$components/base/Avatar.astro'; 3 | import NavLink from '$components/base/NavLink.astro'; 4 | 5 | interface Link { 6 | href: string; 7 | text: string; 8 | icon: string; 9 | } 10 | 11 | const links = [ 12 | { href: '/', text: 'Home', icon: 'home' }, 13 | { href: '/about', text: 'About', icon: 'information' }, 14 | { href: '/articles', text: 'Articles', icon: 'pencil' }, 15 | { href: '/contact', text: 'Contact', icon: 'link-variant' }, 16 | ] satisfies Link[]; 17 | --- 18 | 19 |
20 |
21 | 26 | 27 | 28 | 29 |
30 |

Sapphic Angels

31 |

(she/it/they)

32 |
33 |
34 | 35 | 42 |
43 | -------------------------------------------------------------------------------- /src/components/base/NavLink.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from 'astro/types'; 3 | 4 | interface Props extends HTMLAttributes<'a'> { 5 | class: string; 6 | text: string; 7 | } 8 | 9 | const { text, href, class: className, ...props } = Astro.props; 10 | 11 | let isActive: boolean; 12 | 13 | if (!href) { 14 | isActive = false; 15 | } else if (href === '/') { 16 | isActive = href === Astro.url.pathname; 17 | } else if (Astro.url.pathname.includes('/article')) { 18 | isActive = href === '/articles'; 19 | } else { 20 | isActive = Astro.url.pathname.includes(typeof href === 'string' ? href : href.href); 21 | } 22 | --- 23 | 24 | 30 | 31 | {text} 32 | 33 | -------------------------------------------------------------------------------- /src/components/blog/Card.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { format } from 'date-fns'; 3 | import { Icon } from 'astro-icon/components'; 4 | import Tag from '$components/blog/Tag.astro'; 5 | 6 | const formatDate = (date: string | Date) => format(new Date(date), 'MMMM d, yyyy'); 7 | 8 | interface Props { 9 | data: { 10 | title: string; 11 | description: string; 12 | created: string | Date; 13 | tags: any[]; 14 | }; 15 | id: string; 16 | minutesRead: string; 17 | } 18 | 19 | const { 20 | data: { title, description, created, tags }, 21 | id, 22 | minutesRead, 23 | } = Astro.props; 24 | --- 25 | 26 |
27 |
28 | {title} 29 | 30 |

{description}

31 | 32 |
33 | 34 | 35 | {tags.map((tag) => )} 36 |
37 | 38 | 49 |
50 |
51 | 52 | 57 | -------------------------------------------------------------------------------- /src/components/blog/Comments.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { blog } from '$config'; 3 | --- 4 | 5 |
6 | 7 | -------------------------------------------------------------------------------- /src/components/blog/OGImage.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title: string; 4 | description: string; 5 | created: string | Date; 6 | } 7 | 8 | const { title, description, created } = Astro.props; 9 | --- 10 | 11 |
12 |
13 | {created} 14 |

{title}

15 |

{description}

16 |
17 | 18 |
19 |
20 | 21 | 22 | 23 |
24 | Sapphic Angels 25 | (she/it/they - plural) 26 |
27 |
28 |
29 | 34 |
35 | Article on 36 | sapphic.moe 37 |
38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /src/components/blog/Pagination.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from 'astro-icon/components'; 3 | 4 | const { page } = Astro.props; 5 | --- 6 | 7 | { 8 | page.url.next && ( 9 | 15 | ) 16 | } 17 | 18 | { 19 | page.url.prev && ( 20 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/blog/RSS.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getEntry, render } from 'astro:content'; 3 | const { id } = Astro.params; 4 | 5 | const entry = await getEntry('articles', id!)!; 6 | const { Content } = await render(entry); 7 | --- 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/blog/Tag.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | tag: string; 4 | } 5 | 6 | const { tag } = Astro.props; 7 | --- 8 | 9 | 10 | #{tag} 11 | 12 | -------------------------------------------------------------------------------- /src/components/home/Donation.astro: -------------------------------------------------------------------------------- 1 | 80 | -------------------------------------------------------------------------------- /src/components/home/Friends.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Image } from 'astro:assets'; 3 | import { friends } from '~/utils/friends'; 4 | --- 5 | 6 |
7 | { 8 | friends.map((friend) => { 9 | return ( 10 | 11 | {`Webring 18 | 19 | ); 20 | }) 21 | } 22 |
23 | 24 | 29 | -------------------------------------------------------------------------------- /src/components/markdown/Image.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Image as AstroImage } from 'astro:assets'; 3 | import { loadImage } from '$utils/image'; 4 | import Zoom from 'react-medium-image-zoom'; 5 | import 'react-medium-image-zoom/dist/styles.css'; 6 | 7 | interface Props { 8 | src: string; 9 | alt: string; 10 | } 11 | 12 | const { src, alt } = Astro.props; 13 | if (!src) throw new Error('Property "src" is missing.'); 14 | 15 | const { id } = Astro.params; 16 | 17 | const image = await loadImage(`/posts/${id}/${src}`); 18 | --- 19 | 20 | 21 |
22 | 23 | 24 |
{alt}
25 |
26 |
27 | -------------------------------------------------------------------------------- /src/components/markdown/Spoiler.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from 'astro-icon/components'; 3 | import Image from '$components/markdown/Image.astro'; 4 | 5 | interface Props { 6 | description: string; 7 | image: string; 8 | caption: string; 9 | } 10 | 11 | const { description, image, caption } = Astro.props; 12 | --- 13 | 14 |
15 |
16 | 17 |
18 | Spoiler: 19 | {description} 20 |
21 | 22 |
23 |
36 |
37 | 38 | {caption} 39 |
40 |
41 | 42 | 52 | -------------------------------------------------------------------------------- /src/components/react/ZoomContent.tsx: -------------------------------------------------------------------------------- 1 | const ZoomContent = (props) => { 2 | console.log(props.img.props); 3 | return ( 4 |
5 | {props.img} 6 | 7 |
{props.img.props.alt}
8 |
9 | ); 10 | }; 11 | 12 | export default ZoomContent; 13 | -------------------------------------------------------------------------------- /src/components/react/ZoomWrapper.tsx: -------------------------------------------------------------------------------- 1 | import Zoom from 'react-medium-image-zoom'; 2 | import 'react-medium-image-zoom/dist/styles.css'; 3 | import ZoomContent from './ZoomContent'; 4 | 5 | export default function ZoomWrapper(props) { 6 | return ( 7 | 8 |
9 | {props.image} 10 | 11 | {props.alt} 12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/content.config.ts: -------------------------------------------------------------------------------- 1 | import { z, defineCollection } from 'astro:content'; 2 | import { glob } from 'astro/loaders'; 3 | 4 | const mdLoader = (collection: string) => 5 | glob({ pattern: '**/[^_]*.{md,mdx}', base: `./src/content/${collection}` }); 6 | 7 | const articlesCollection = defineCollection({ 8 | loader: mdLoader('articles'), 9 | schema: z.object({ 10 | title: z.string(), 11 | description: z.string(), 12 | draft: z.boolean().optional().default(false), 13 | created: z.string().or(z.date()), 14 | modified: z.string().or(z.date()).optional(), 15 | tags: z.array(z.any()), 16 | comments: z.boolean().optional().default(true), 17 | }), 18 | }); 19 | 20 | const infoCollection = defineCollection({ 21 | loader: mdLoader('info'), 22 | schema: z.object({ 23 | title: z.string(), 24 | description: z.string(), 25 | modified: z.string().or(z.date()).optional(), 26 | }), 27 | }); 28 | 29 | export const collections = { 30 | articles: articlesCollection, 31 | info: infoCollection, 32 | }; 33 | -------------------------------------------------------------------------------- /src/content/articles/oneshot-on-linux.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'OneShot on Linux: The Definitive Guide' 3 | description: >- 4 | The resource to help you play, complete, and enjoy OneShot on Linux 5 | distrubutions. 6 | created: 2024-04-03 7 | modified: 2024-04-04 8 | tags: 9 | - oneshot 10 | - linux 11 | comments: true 12 | --- 13 | *Note: this guide assumes you have the *[*Steam version*](https://store.steampowered.com/app/420530/OneShot/)* of OneShot.* 14 | *For the [itch.io version](https://futurecat.itch.io/oneshot) of OneShot, please see the "[I cannot get the itch.io version to run!](#i-cannot-get-the-itchio-version-to-run)" section.* 15 | 16 | OneShot is one of our favourite games out there. It helped get us through some of our darkest times and it's our comfort game. We could spend hours talking about it. 17 | 18 | Unfortunately, getting it to work on Linux can be quite the challenge. Though the game has a native Linux version, you have to delete certain files for it to launch - but even that is not a guarantee that it will work on all Linux distributions. 19 | 20 | It's simply not feasible for an average user to have to scour the wide Internet for possible solutions just to be able to play OneShot. 21 | 22 | That's why we have decided to write an article guide to make sure one can enjoy the game on Linux with close to no hurdles. 23 | 24 | ## Table of Contents 25 | 26 | ## Caveats 27 | 28 | Unfortunately, due to the nature of the game, your immersion may be slightly ruined. 29 | 30 | For example, the game automatically changing your desktop wallpaper during a certain puzzle won't work on Linux. See the [Desktop wallpaper not changing](#my-desktop-wallpaper-is-not-updating) section for more information. 31 | 32 | OneShot is best played and experienced on a Windows environment. 33 | 34 | If you don't mind this trade-off though or simply don't care, keep on reading. 35 | 36 | ## Prerequisites 37 | 38 | You will need to install [Protontricks](https://github.com/Matoking/protontricks). 39 | 40 | ## Installing Protontricks 41 | 42 | There are several ways to install Protontricks. 43 | 44 | *If you already have Protontricks installed, you can skip to the *[*Running the game*](#running-the-game)* section.* 45 | 46 | ### Method 1: Installing Protontricks via Flatpak 47 | 48 | If you have Flatpak installed, you can run the following command to install Protontricks: 49 | 50 | ```bash title="Terminal" 51 | flatpak install flathub com.github.Matoking.protontricks 52 | ``` 53 | 54 | To install Flatpak for your Linux distribution, you can follow [Flathub's guide](https://flathub.org/setup). 55 | 56 | ### Method 2: Installing Protontricks as a system package 57 | 58 | Alternatively, you can install Protontricks as a system package. 59 | 60 | Here are the following commands to install Protontricks for the most popular Linux distributions: 61 | 62 | * **Arch Linux**: Protontricks is available via the [AUR](https://aur.archlinux.org/packages/protontricks). 63 | 64 | * paru: `paru -S protontricks` 65 | 66 | * yay: `yay -S protontricks` 67 | 68 | * **Debian 12+**: `sudo apt install protontricks` 69 | 70 | * **Fedora 32+**: `sudo dnf install protontricks` 71 | 72 | * **NixOS 21+**: `nix-env -iA protontricks` 73 | 74 | * **Ubuntu 22+**: `sudo apt install protontricks` 75 | 76 | * **Void Linux**: `xbps-install protontricks` 77 | 78 | If your Linux distribution is not listed here, check out the ["Installation" section on the Protontricks repository](https://github.com/Matoking/protontricks?tab=readme-ov-file#installation) for more information. 79 | 80 | ## Running the game 81 | 82 | We'll be using Valve's Proton compatibility layer to play OneShot. 83 | 84 | For the majority of the game, you won't have to use **Protontricks**. It's only near the end that you'll be forced to run a certain executable file in order to progress further. [*(Jump to 'Later stages of the game' section)*](#later-stages-of-the-game) 85 | 86 | Head over to your library, right click OneShot, select "**Properties..."** and head over to the **Compatibility** tab. 87 | 88 | Enable "**Force the use of a specific Steam Play compatibility tool**", and select **Proton 7.0-6**. This will download that specific Proton version, which might take a while. 89 | 90 | The Steam compatibility tab for OneShot. 94 | 95 | Once that's done, you can launch OneShot from your Steam library and the majority of the game will run just fine! 96 | 97 | ## Interacting with documents 98 | 99 | In some parts of the game, you may have to interact with files. Usually, these files are located in the **My Documents** folder on Windows. 100 | 101 | With Proton, your documents for OneShot are located in the `~/.steam/steam/steamapps/compatdata/420530/pfx/drive_c/users/steamuser/Documents` folder. 102 | 103 | I'm sure you don't feel like memorizing that, so let's create a symlink (think of it as a shortcut, basically) for easy access! 104 | 105 | We'll be creating a symlink in your home directory. 106 | Run the following command to create a **ProtonDocuments** symlink in your Documents folder: 107 | 108 | ```bash title="Terminal" 109 | ln -s ~/.steam/steam/steamapps/compatdata/420530/pfx/drive_c/users/steamuser/Documents ~/Documents/ProtonDocuments 110 | ``` 111 | 112 | Now whenever the game creates a document file, you can always access it under your Documents folder! (`~/Documents/ProtonDocuments`) 113 | 114 | GNOME File Explorer (Nautilus) showing the ProtonDocuments symlink in your home directory's Documents folder. 118 | 119 | 120 | You may see additional folders like Downloads, Templates, Videos and so on in the **ProtonDocuments** folder, but don't worry about them! 121 | 122 | ## Later stages of the game 123 | 124 | Though most of the game will work and play fine, eventually, you'll have to run a file named `_______.exe` (we'll be calling this the "clover program" for the rest of the guide) in order to progress further in the game. This is where Protontricks comes into play. 125 | 126 | Using `protontricks-launch`, run the following command to launch the clover program: 127 | 128 | ```bash title="Terminal" 129 | protontricks-launch --appid 420530 ~/.steam/steam/steamapps/compatdata/420530/pfx/drive_c/users/steamuser/Documents/My\ Games/Oneshot/_______.exe 130 | ``` 131 | 132 | Afterwards, launch OneShot via its `steamshim` binary: 133 | 134 | ```bash title="Terminal" 135 | protontricks-launch --appid 420530 ~/.steam/steam/steamapps/common/OneShot/steamshim.exe 136 | ``` 137 | 138 | **Note**: The Steam overlay will be unavailable under Protontricks, however achievements will work pretty much just fine. 139 | 140 | ### Aliasing the commands 141 | 142 | If you want to have these commands at an easy reach, you can add them as an alias to your shell file. In our case, we'll be using Bash, but you can modify the example below to work with zsh, fish, and other shells. 143 | 144 | Open the `~/.bashrc` file with the editor of your choice, and add the following code to the end of the file: 145 | 146 | ```sh title=".bashrc" 147 | alias launch-oneshot=" 148 | protontricks-launch --appid 420530 ~/.steam/steam/steamapps/common/OneShot/steamshim.exe 149 | " 150 | 151 | alias launch-oneshot-clover=" 152 | protontricks-launch --appid 420530 ~/.steam/steam/steamapps/compatdata/420530/pfx/drive_c/users/steamuser/Documents/My\ Games/Oneshot/_______.exe 153 | " 154 | ``` 155 | 156 | Now you can launch OneShot and the clover program by just typing `launch-oneshot` and `launch-oneshot-clover` respectively in your terminal! 157 | 158 | 163 | 164 | 165 | 166 | ## Questions and answers 167 | 168 | ### I'm getting the "Failed to initialize Steamworks" error! 169 | 170 | You need to have Steam running on your host machine when you execute the `steamshim` file. 171 | 172 | ### I cannot get the itch.io version to run! 173 | 174 | Due to an AppImageKit bug, the .appimage file provided on the itch.io version of OneShot is broken. 175 | 176 | For the time being, you can use the following snippet provided by [Leo](https://vriska.dev). 177 | 178 | ```sh frame="code" title="AppImageScript.sh" 179 | ./OneShot.AppImage --appimage-extract 180 | 181 | rm squashfs-root/usr/lib/libgmodule-2.0.so.0 182 | rm squashfs-root/usr/lib/libgtk-x11-2.0.so.0 183 | rm squashfs-root/usr/lib/libgdk-x11-2.0.so.0 184 | 185 | wget https://archive.archlinux.org/packages/l/libffi/libffi-3.3-4-x86_64.pkg.tar.zst 186 | mkdir libffi 187 | tar -xC libffi/ -f libffi-3.3-4-x86_64.pkg.tar.zst 188 | cp libffi/usr/lib/libffi.so.7.1.0 squashfs-root/usr/lib/libffi.so.7 189 | 190 | ./squashfs-root/usr/bin/oneshot 191 | ``` 192 | 193 | ### Where can I view my save files for OneShot? 194 | 195 | Your save and config files for OneShot are located in the `~/.steam/steam/steamapps/compatdata/420530/pfx/drive_c/users/steamuser/AppData/Roaming/Oneshot` folder. 196 | 197 | If you end up interacting with your save files often, you can create run the following command: 198 | 199 | ```bash title="Terminal" 200 | ln -s ~/.steam/steam/steamapps/compatdata/420530/pfx/drive_c/users/steamuser/AppData/Roaming/Oneshot ~/Documents/OneShotSaves 201 | ``` 202 | 203 | This will create a **OneShotSaves** symlink in your Documents folder for easy access: 204 | 205 | The OneShotSaves symlink in your home directory's Documents folder. It contains your save files for OneShot, as well as some configuration files. 209 | 210 | ### My desktop wallpaper is not updating! 211 | 212 | In the native Linux version of OneShot and on Windows, the game is able to automatically change your wallpaper during a certain puzzle in the Glen region. 213 | 214 | Unfortunately, this is not possible with Proton as the game is running on Wine, which, to our knowledge anyway, is not able to change the host machine's desktop wallpaper. 215 | 216 | We have not found a solution for this issue. 217 | For the time being, you can change the wallpaper manually by going to the `~/.steam/steam/steamapps/common/OneShot/Wallpaper` folder and selecting `desktop.png` as your wallpaper. You can change the wallpaper back after you're done with the puzzle. 218 | 219 | Alternatively, you can just open the image for the duration of the puzzle. 220 | 221 | ### Can I play Solstice with this? 222 | 223 | Yes. Just delete the `save_progress.oneshot` file in your `ProtonDocuments/My Games/Oneshot` folder and execute the `steamshim.exe` file with Protontricks (or run the `launch-oneshot` alias command above). 224 | 225 | ### I know how to fix X or I want to include Y in this guide! 226 | 227 | Cool! Just leave a comment below or contact me and I'll update the article. 228 | 229 | ## Concluding thoughts 230 | 231 | As much as we love OneShot, we honestly wish it was updated to work well to run natively on Linux just fine with no additional tweaking, or at the very least, updating the game to work on the latest Proton versions. 232 | 233 | No hate towards the developers of the game though! We know it must be pretty hard to support the Linux version of OneShot and ensure it runs well across all distributions and configurations. 234 | 235 | Thank you for reading 'till the end! 236 | 237 | If you want to support us, please donate to [our Ko-fi page](https://ko-fi.com/solelychloe) as we're trying to run away from our abusive family. 238 | -------------------------------------------------------------------------------- /src/content/articles/using-cloudflare-r2-on-owncloud.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using Cloudflare R2 as your primary storage on Owncloud 3 | description: A guide on how to set up Cloudflare R2 on Owncloud and use it as your storage. 4 | created: '2023-04-02' 5 | modified: '2024-03-24' 6 | tags: 7 | - cloudflare 8 | - owncloud 9 | comments: true 10 | --- 11 | ## Introduction 12 | 13 | We have a compute instance that is running [Owncloud](https://owncloud.com), which we use to backup our files and whatnot. While we were configuring the instance and uploading our files, we quickly realized that our instance's storage is very limited and did not have enough space to actually house all of our potential future backups. 14 | 15 | But then we had an idea: what if we could hook up [Cloudflare's R2 service](https://www.cloudflare.com/products/r2/) to Owncloud? 16 | 17 | Stick with us as we show you how to do exactly that in this article. 18 | 19 | ## Table of contents 20 | 21 | ## Preamble 22 | 23 | **💡 This guide assumes you're running an instance of Owncloud on a Docker container using docker-compose.** 24 | 25 | If you don't have an instance of Owncloud running on Docker, follow these steps: 26 | 27 | * You'll need to install Docker first. The official Docker Docs page provides [a guide](https://docs.docker.com/engine/install/#server) on how to install Docker on just about every system. 28 | 29 | * Then, head over to Owncloud's [documentation page](https://doc.owncloud.com/server/10.12/admin_manual/installation/docker) and install Owncloud on a Docker container. 30 | 31 | ## Creating a bucket 32 | 33 | Go to your [Cloudflare dashboard](https://dash.cloudflare.com) and navigate to the R2 page. Then click on the **Create bucket** 34 | button. 35 | 36 | **💡 When creating a bucket, make sure you give this bucket a unique name because you won't be able to change it afterwards!** 37 | 38 | For the purposes of this guide, we'll stick with the bucket name `owncloud-instance`. 39 | 40 | ![The unique name for your Cloudflare R2 bucket.](https://elixi.re/i/eq5h5.png "The unique name for your Cloudflare R2 bucket.") 41 | 42 | Once you're done with that, head back to the R2 page, click on the **Manage R2 API Tokens** link and click on the **Create API token** button. 43 | 44 | ### Bucket creation options 45 | 46 | Let's go through each of the options on the page: 47 | 48 | * **Token name**: This is not that important, but if you want to be able to easily identify the R2 token on this page in the future, you can set a unique name here. 49 | 50 | * **Permissions**: Make sure you set this to **Edit**! Otherwise you may run into permission issues when uploading files. 51 | 52 | * **TTL**: If you just want to create this token and forget about it, set this to **Infinity**. Otherwise, this option is up to you. 53 | 54 | When you're done, click on the **Create API Token** button. 55 | 56 | You'll be able to see your **Access Key ID** and **Secret Access Key** values here. Keep them somewhere safe, as you will not be able to see them again! 57 | 58 | ![The R2 API token being successfully created with the "Access Key ID" and "Secret Access Key" values provided.](https://elixi.re/i/3rlfx.png "The R2 API token being successfully created with the \"Access Key ID\" and \"Secret Access Key\" values provided.") 59 | 60 | ## Installing the S3 extension for Owncloud 61 | 62 | Once you're done creating an R2 bucket, head over to your Owncloud admin dashboard. 63 | 64 | Click on the menu bar located in the upper left corner, and select the **Market** tab. 65 | 66 | ![The Market tab on your Owncloud instance.](https://elixi.re/i/7zakj.png "The Market tab on your Owncloud instance.") 67 | 68 | Scroll down until you find the **S3 Primary Object Storage** extension, and install it. 69 | 70 | ![The S3 Primary Object Storage extension on the Owncloud Marketplace.](https://elixi.re/i/rnmm8.png "The S3 Primary Object Storage extension on the Owncloud Marketplace.") 71 | 72 | **💡 Note, you'll want this extension and not the extension called "External Storage: S3"!** 73 | 74 | ## Setting up R2 for your Owncloud instance 75 | 76 | **⚠️ Before proceeding any further, make sure you back up all your data! Proceeding with the steps below will wipe any data you had on your Owncloud instance.** 77 | 78 | You're done with all that? Great. It's time to configure R2 to work on your Owncloud instance. 79 | 80 | Run the following command to access the filesystem of your Owncloud Docker instance: 81 | 82 | ```bash title="Terminal" 83 | > docker exec -t -i owncloud_server /bin/bash 84 | ``` 85 | 86 | Head over to the **config** folder. The **config** folder should have the following files: 87 | 88 | ```bash title="Terminal" 89 | > ls 90 | config.php objectstore.config.php overwrite.config.php 91 | ``` 92 | 93 | We're looking to edit the **config.php** file. Add the following to the configuration file: 94 | 95 | ```php title="config.php" 96 | 'objectstore' => [ 97 | 'class' => '\\OC\\Files\\ObjectStore\\S3', 98 | 'arguments' => [ 99 | 'bucket' => 'BUCKET', 100 | 'autocreate' => true, 101 | 'key' => 'ACCESS_KEY_ID', 102 | 'secret' => 'SECRET_ACCESS_KEY', 103 | 'hostname' => 'ACCOUNT_ID.r2.cloudflarestorage.com', 104 | 'port' => 443, 105 | 'use_ssl' => true, 106 | 'region' => 'auto', 107 | ], 108 | ] 109 | ``` 110 | 111 | ### S3 configuration options 112 | 113 | You'll want to replace the following options: 114 | 115 | * **bucket**: This is your unique R2 bucket name that you created earlier. *(I used ****`owncloud-instance`**** for this guide)* 116 | 117 | * **key**: This is the Access Key ID that you obtained when creating the R2 bucket. 118 | 119 | * **secret**: This is the Secret Access Key that you obtained when creating the R2 bucket. 120 | 121 | * **hostname**: You can find the bucket hostname on the bucket page itself. Make sure to only include the URL name! (the `/owncloud-instance` bit from the URL can be removed) 122 | 123 | ![The R2 bucket hostname.](https://elixi.re/i/y8e7l.png "The R2 bucket hostname.") 124 | 125 | Then, run `docker restart owncloud_server` to apply the changes. 126 | 127 | And that's it! Log back in to your Owncloud admin dashboard, and your instance should utilize your Cloudflare R2 bucket as its primary storage. 128 | 129 | ## Conclusion 130 | 131 | Overall, it's pretty painless for your Owncloud instance to utilize Cloudflare's R2 service. 132 | 133 | We will warn you though, Cloudflare's base plan for R2 only includes 10 GB of storage per month. 134 | 135 | ![The pricing plans for Cloudflare's R2 service.](https://elixi.re/i/kwujy.png "The pricing plans for Cloudflare's R2 service.") 136 | 137 | But it all depends if you're fine with that. :) 138 | 139 | --- 140 | 141 | *Hey! If you're still here...* 142 | 143 | *This is the first time we're writing a blog post of this sort. Let us know if we did well, or if there's anything we can improve with future blog posts. Thank you! ❤️* 144 | -------------------------------------------------------------------------------- /src/content/info/about.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: About 3 | description: A little more info about us. 4 | --- 5 | 6 | ### Who? 7 | 8 | Hi there! We're the **Sapphic Angels** system. We're computer science students at [Almaty Management University][almau] in Kazakhstan, specializing in Software Engineering. 9 | 10 | We used to go by a lot of names before. You might recognize the usernames **solely** or **solelychloe** before we were plural. 11 | 12 | We have a fascination for anything related to system administration and we love to dabble with anything related to TypeScript, Nix, and Astro. 13 | And as hobbyist sysadmins, we self-host software for our specialized needs. 14 | 15 | Due to our impaired eyesight, it's our mission to strive for accessibility where possible! We want to start by providing alt text where possible for our services. For example, the images that you see in our articles all have alt text in them. 16 | 17 | [almau]: https://almau.edu.kz 'Almaty Management University' 18 | 19 | ### What do you do? 20 | 21 | We're currently members of [Dijit][dijit], an unannounced and upcoming Australian-based software company. 22 | 23 | Here are some open-source projects we've contributed to: 24 | 25 | - [simple-fm][simple-fm] 26 | A simple, asynchronous Last.fm library written in TypeScript. 27 | _Made in TypeScript._ 28 | - [tailwindcss.com][tailwind-site] 29 | Rapidly build modern websites without ever leaving your HTML. 30 | _Made in Next.js._ 31 | - [parallel.report][parallel] 32 | Track Guardians who have the Parallel Program emblem in Destiny 2. 33 | _Made in Astro, Tailwind CSS and TypeScript._ 34 | - [sapphic.moe][sapphic] 35 | The site you're viewing right now! 36 | _Made in Astro and Tailwind CSS._ 37 | - [Felicity][felicity] 38 | Home page for Felicity, a powerful Discord bot that's designed to enhance your Destiny 2 gaming 39 | experience. 40 | _Made in Astro and Starlight._ 41 | - [Unofficial Last.fm API Docs][lfm-docs] 42 | Community-made docs for the Last.fm API, with a goal of being up-to-date and as accurate as possible. 43 | _Made in MkDocs._ 44 | 45 | [dijit]: https://github.com/dijitco 'Dijit' 46 | [tailwind-site]: https://tailwindcss.com 'Website for TailwindCSS' 47 | [simple-fm]: https://simple.sapphic.moe 'Website for simple-fm' 48 | [parallel]: https://parallel.report 'Parallel Program Report' 49 | [sapphic]: https://sapphic.moe "Sapphic Angels' website" 50 | [felicity]: https://felicity.pages.dev 'Felicity' 51 | [lfm-docs]: https://lastfm-docs.github.io/api-docs 'Unofficial Last.fm API docs' 52 | 53 | 54 | ### What do you like? 55 | 56 | We like to play video games in our spare time! Our favourite games at the moment are: 57 | 58 | - [CrossCode][crosscode] 59 | - [Life is Strange][lis] 60 | - [Life is Strange: Before the Storm][lisbts] 61 | - [OMORI][omori] 62 | - [OneShot][oneshot] 63 | - [Portal 2][p2] 64 | - [SIGNALIS][signalis] 65 | 66 | [crosscode]: https://store.steampowered.com/app/368340 'CrossCode' 67 | [lis]: https://store.steampowered.com/app/319630 'Life is Strange' 68 | [lisbts]: https://store.steampowered.com/app/554620 'Life is Strange: Before the Storm' 69 | [omori]: https://store.steampowered.com/app/1150690 'OMORI' 70 | [oneshot]: https://store.steampowered.com/app/420530 'OneShot' 71 | [p2]: https://store.steampowered.com/app/620 'Portal 2' 72 | [signalis]: https://store.steampowered.com/app/1262350/ 'SIGNALIS' 73 | 74 | We like all sorts of music! Here are some of our favourite artists and bands at the moment: 75 | 76 | - [Audioslave][audioslave] 77 | - [Enter Shikari][entershikari] 78 | - [HOME][home] 79 | - [Justice][justice] 80 | - [Kero Kero Bonito][kkb] 81 | - [Lemon Demon][lemondemon] 82 | - [Paramore][paramore] 83 | - [Porter Robinson][porter] 84 | - [Soundgarden][soundgarden] 85 | - [Vanilla][vanilla] 86 | 87 | [audioslave]: https://www.youtube.com/@audioslaveofficial 'Audioslave' 88 | [entershikari]: https://www.youtube.com/@entershikari 'Enter Shikari' 89 | [home]: https://soundcloud.com/home-2001 'HOME' 90 | [justice]: https://www.youtube.com/@Justice 'Justice' 91 | [kkb]: https://www.youtube.com/@KeroKeroBonito 'Kero Kero Bonito' 92 | [lemondemon]: https://www.youtube.com/@neilcic 'Lemon Demon' 93 | [paramore]: https://www.youtube.com/@Paramore 'Paramore' 94 | [porter]: https://www.youtube.com/@PorterRobinson 'Porter Robinson' 95 | [soundgarden]: https://www.youtube.com/@Soundgarden 'Soundgarden' 96 | [vanilla]: https://vanillabeats.bandcamp.com/music 'Vanilla' 97 | 98 | You can check out our [Last.fm profile][lastfm] for more info. 99 | 100 | [lastfm]: https://last.fm/user/solelychloe 'Our Last.fm profile page. (solelychloe)' 101 | 102 | ## Miscellaneous 103 | 104 | ### That's a cool 88x31 button. How can I include it to my site? 105 | 106 | You can copy-paste the following HTML snippet to your website: 107 | 108 | ```html title="HTML snippet" 109 | 110 | 111 | 112 | ``` -------------------------------------------------------------------------------- /src/content/info/contact.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contact 3 | description: If you're trying to reach us, you can do so here through one of the options below. 4 | --- 5 | 6 | ### Immediate response 7 | If you want our immediate attention, the options below will work. 8 | 9 | - Discord: [@sapphic.moe][discord] 10 | - Mail: [contact@sapphic.moe][mail] 11 | - Twitter: [@SapphoSys][twitter] 12 | 13 | [discord]: https://discord.com/users/312145496179474434 'Our Discord profile. (@sapphic.moe)' 14 | [mail]: mailto:contact@sapphic.moe 'Our email address. (contact@sapphic.moe)' 15 | [twitter]: https://twitter.com/SapphoSys 'Our Twitter page. (@SapphoSys)' 16 | 17 | ### Less urgent 18 | If you think you can wait, reach out using the options below. 19 | 20 | - Signal: [@chloe.01][signal] 21 | - Mastodon: [@chloe@wetdry.world][mastodon] 22 | 23 | [mastodon]: https://wetdry.world/@sapphic 'Our Mastodon page. (@chloe@wetdry.world)' 24 | [signal]: https://signal.me/#eu/SdSBoztdGjRkd6EP60LP3zPdAeOrbaeMn-2rFeVBQTFi2qOcH6pOrKuDTUNvNSBJ 'Our Signal profile. (@chloe.01)' 25 | 26 | ### Other forms of contact 27 | We are registered on these platforms, but the times that we check them are rare. 28 | 29 | - Bluesky: [@sapphic.moe][bsky] 30 | - Matrix: @sapphic:matrix.org 31 | 32 | [bsky]: https://bsky.app/profile/sapphic.moe 'Our Bluesky page. (@sapphic.moe)' 33 | 34 | ### Places to find us 35 | 36 | - Codeberg: [@sapphic][codeberg] 37 | - GitHub: [@SapphicMoe][github] 38 | 39 | [codeberg]: https://codeberg.org/sapphic 'Our Codeberg page. (@sapphic)' 40 | [github]: https://github.com/SapphicMoe 'Our GitHub page. (@SapphicMoe)' -------------------------------------------------------------------------------- /src/layouts/Article.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '$layouts/Page.astro'; 3 | 4 | import { base, blog } from '$config'; 5 | import { format } from 'date-fns'; 6 | import { Icon } from 'astro-icon/components'; 7 | 8 | import Comments from '$components/blog/Comments.astro'; 9 | import Tag from '$components/blog/Tag.astro'; 10 | 11 | import '@fontsource/iosevka/400.css'; 12 | import '$styles/article.css'; 13 | 14 | interface Props { 15 | id?: string; 16 | 17 | title: string; 18 | description: string; 19 | draft?: boolean; 20 | created: string | Date; 21 | modified?: string | Date; 22 | tags: any[]; 23 | minutesRead?: string; 24 | 25 | comments?: boolean; 26 | } 27 | 28 | const { id, title, description, draft, created, modified, tags, minutesRead, comments } = 29 | Astro.props; 30 | 31 | const checkComments = () => blog.comments.enabled && comments; 32 | 33 | const formatDate = (date: string | Date) => format(new Date(date), 'MMMM d, yyyy'); 34 | const createdAt = created ? formatDate(created) : undefined; 35 | const modifiedAt = modified ? formatDate(modified) : undefined; 36 | 37 | const backlinkPath = Astro.url.pathname.replace('/article', ''); 38 | const backlink = new URL(backlinkPath, 'https://solstice.tf').href; 39 | 40 | const image = id 41 | ? { 42 | src: `/article/${id}.png`, 43 | alt: title, 44 | } 45 | : { 46 | src: base.images.favicon.fileName, 47 | alt: base.images.favicon.altText, 48 | }; 49 | --- 50 | 51 | 58 | {checkComments() && } 59 | 60 | { 61 | draft && ( 62 |
63 |
64 | 65 |

Warning

66 |
67 | 68 | This article is a draft and is not ready for public viewing. 69 |
70 | ) 71 | } 72 | 73 |
74 |
75 |

{title}

76 |

{description}

77 |
78 | 79 |
80 |

84 | 85 | {createdAt} 86 |

87 | 88 | 89 | 90 | 91 | {tags.map((tag) => )} 92 | 93 | 94 |

95 | 96 | {minutesRead} 97 |

98 |
99 |
100 | 101 |
102 | 103 |
104 | 105 | {checkComments() && } 106 |
107 | -------------------------------------------------------------------------------- /src/layouts/ArticleList.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '$layouts/Page.astro'; 3 | 4 | import BlogCard from '$components/blog/Card.astro'; 5 | 6 | import { Icon } from 'astro-icon/components'; 7 | import { type CollectionEntry, render, getEntry } from 'astro:content'; 8 | interface Props { 9 | tag?: string | number; 10 | articles: CollectionEntry<'articles'>[]; 11 | } 12 | 13 | const { tag, articles } = Astro.props; 14 | --- 15 | 16 | 21 |
22 |

Articles

23 |

A personal blog, comprising of whatever stuff we feel like rambling about.

24 | 25 | { 26 | tag && ( 27 |
28 | 29 | 30 | Showing {tag} articles. 31 | 32 | [Close] 33 |
34 | ) 35 | } 36 |
37 | 38 | { 39 | articles 40 | .filter(({ data }) => !data.draft) 41 | .sort((a, b) => new Date(b.data.created).valueOf() - new Date(a.data.created).valueOf()) 42 | .map(async ({ data, id }) => { 43 | const entry = await getEntry('articles', id!)!; 44 | 45 | const { 46 | remarkPluginFrontmatter: { minutesRead }, 47 | } = await render(entry); 48 | 49 | return ; 50 | }) 51 | } 52 | 53 | 54 |
55 | -------------------------------------------------------------------------------- /src/layouts/Markdown.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '$layouts/Page.astro'; 3 | 4 | import '@fontsource/iosevka/400.css'; 5 | import '$styles/article.css'; 6 | 7 | interface Props { 8 | title: string; 9 | description?: string; 10 | } 11 | 12 | const { title, description } = Astro.props; 13 | --- 14 | 15 | 16 |
17 |

{title}

18 |

{description}

19 |
20 | 21 |
22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /src/layouts/Page.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { slide } from 'astro:transitions'; 3 | 4 | import Footer from '$components/base/Footer.astro'; 5 | import Head from '$components/base/Head.astro'; 6 | import NavBar from '$components/base/NavBar.astro'; 7 | 8 | import '@fontsource/atkinson-hyperlegible/400.css'; 9 | 10 | interface Props { 11 | description?: string; 12 | pageTitle: string; 13 | title?: string; 14 | created?: string; 15 | ogImage?: { 16 | src?: string; 17 | alt?: string; 18 | }; 19 | } 20 | 21 | const { 22 | description = 'Software engineer, sysadmin, community manager.', 23 | pageTitle, 24 | title = 'Sapphic Angels', 25 | created, 26 | ogImage, 27 | } = Astro.props; 28 | --- 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
44 | 45 |
46 | 47 |