your personal website on atproto - mirror blento.app

small fixes, more docs

Florian 16a8feff 1d2c5f48

+143 -53
+23 -11
README.md
··· 1 # blento 2 3 - WORK IN PROGRESS, not ready for use yet, but you can test it out at: https://blento.app (no guarantee that your blento wont be broken at some point though and might have to be recreated). 4 5 - your personal website in a bento style layout, using your bluesky PDS as a backend. 6 7 made with svelte, tailwind and hosted on cloudflare workers. 8 9 - ## Development 10 11 - ``` 12 - git clone https://github.com/flo-bit/blento.git 13 - cd blento 14 - cp .env.example .env 15 - pnpm install 16 - pnpm run dev 17 - ``` 18 19 ## Selfhosting 20 ··· 22 23 ## Making Custom cards 24 25 - See [docs/CustomCards](./docs/CustomCards.md)
··· 1 # blento 2 3 + Alpha version can be tried at https://blento.app 4 5 + Your personal website in a bento style layout, using your bluesky personal data server as a backend. 6 7 made with svelte, tailwind and hosted on cloudflare workers. 8 9 + ## Why? 10 11 + This started as a replacement/alternative for bento.me which is shutting down in a few weeks (Feb 2026) after being bought by a competitor ^^ I wanted to build a version that couldn't just shut down or where it would be very easy to spin up a new version (with all the data) should the old one disappear. 12 + 13 + That's why all your data is saved on your bluesky personal data server, so you can start setting up your website on blento.app, but then anytime you want to start self hosting you easily take your data with you (dedicated forks optimized for self hosting on different platforms coming soon). 14 + 15 + Should blento.app shut down at some point, someone else can also spin up a new version that shows all blentos (note: it's MIT licensed so you *could* do that now too and offer a competing service, but please don't (except for self-hosting your own profile ofc), legal != nice). 16 + 17 + One other note: for most independence I encourage everyone to get their own domain and either self host or redirect to blento.app/your-profile (still working on a way to make this as easy as possible for non-technical users, if you have any suggestions please reach out). 18 19 ## Selfhosting 20 ··· 22 23 ## Making Custom cards 24 25 + See [docs/CustomCards](./docs/CustomCards.md) 26 + 27 + ## Contributing 28 + 29 + See [docs/Contributing](./docs/Contributing.md) 30 + 31 + ## Idea for a card? 32 + 33 + Open an issue 34 + 35 + ## License 36 + 37 + MIT
+4 -4
docs/Beta.md
··· 1 - # Todo for beta 2 3 - fix opengraph image stuff 4 5 - site.standard 6 - - move subpages to own lexicon 7 - move description to markdownDescription and set description as text only 8 9 - - fix recent blentos only showing the last ~5 10 11 - card with big call to action button 12 13 - link card: save favicon and og image as blobs 14 15 - - video card 16 17 - allow changing profile picture 18
··· 1 + # Todo for beta version 2 3 - fix opengraph image stuff 4 5 - site.standard 6 + - move subpages to own lexicon (app.blento.page) 7 - move description to markdownDescription and set description as text only 8 9 + - fix recent blentos only showing the last ~5 blentos 10 11 - card with big call to action button 12 13 - link card: save favicon and og image as blobs 14 15 + - video card? 16 17 - allow changing profile picture 18
+22
docs/Contributing.md
···
··· 1 + # Contributing Guidelines 2 + 3 + Contributions are very welcome 🎉 4 + 5 + For creating new cards see [here](CustomCards.md) (and check out [existing card ideas](CardIdeas.md)) 6 + 7 + ## Development 8 + 9 + ``` 10 + git clone https://github.com/flo-bit/blento.git 11 + cd blento 12 + cp .env.example .env 13 + pnpm install 14 + pnpm run dev 15 + ``` 16 + 17 + ## AI assisted development 18 + 19 + You can submit PRs written with AI but please make sure: 20 + 21 + - there's no extra unnecessary changes/unnecessary verbose code (keep it simple) 22 + - you test everything yourself (in light/dark mode, with and without colored cards, in edit mode and not in edit mode)
+2 -3
docs/CustomCards.md
··· 1 # Custom Cards 2 3 - WORK IN PROGRESS, EARLY STATE, MIGHT CHANGE. 4 - 5 see `src/lib/cards` for how cards are made (and e.g. `src/lib/cards/EmbedCard/` and `src/lib/cards/LivestreamCard/` for examples of implementation). 6 7 Notes: 8 9 - Cards should be styled to work in light and dark mode (with dark: class modifier) as well as when cards are colorful (= bg-color-500 for the card) (with accent: modifier).
··· 1 # Custom Cards 2 3 see `src/lib/cards` for how cards are made (and e.g. `src/lib/cards/EmbedCard/` and `src/lib/cards/LivestreamCard/` for examples of implementation). 4 5 Notes: 6 7 + - Cards should be styled to work in light and dark mode (with dark: class modifier) as well as when cards are colorful (= bg-color-500 for the card) (with accent: modifier). 8 + - Please test newly created cards both when editing (/your-user/edit) and in your user profile when saved (/your-user)
+1 -1
package.json
··· 1 { 2 "name": "blento", 3 "private": true, 4 - "version": "0.1.0", 5 "type": "module", 6 "scripts": { 7 "dev": "vite dev",
··· 1 { 2 "name": "blento", 3 "private": true, 4 + "version": "0.2.0", 5 "type": "module", 6 "scripts": { 7 "dev": "vite dev",
+29
src/lib/cards/BlueskyMediaCard/SidebarItemBlueskyMediaCard.svelte
···
··· 1 + <script lang="ts"> 2 + import { Button } from '@foxui/core'; 3 + 4 + let { onclick }: { onclick: () => void } = $props(); 5 + </script> 6 + 7 + <Button {onclick} variant="ghost" class="w-full justify-start"> 8 + <svg 9 + xmlns="http://www.w3.org/2000/svg" 10 + class="text-accent-600 dark:text-accent-400" 11 + viewBox="0 0 24 24" 12 + fill="none" 13 + stroke="currentColor" 14 + stroke-width="2" 15 + stroke-linecap="round" 16 + stroke-linejoin="round" 17 + ><path d="m22 11-1.296-1.296a2.4 2.4 0 0 0-3.408 0L11 16" /><path 18 + d="M4 8a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2" 19 + /><circle cx="13" cy="7" r="1" fill="currentColor" /><rect 20 + x="8" 21 + y="2" 22 + width="14" 23 + height="14" 24 + rx="2" 25 + /></svg 26 + > 27 + 28 + Bluesky media 29 + </Button>
+3 -1
src/lib/cards/BlueskyMediaCard/index.ts
··· 1 import type { CardDefinition } from '../types'; 2 import BlueskyMediaCard from './BlueskyMediaCard.svelte'; 3 import CreateBlueskyMediaCardModal from './CreateBlueskyMediaCardModal.svelte'; 4 5 export const BlueskyMediaCardDefinition = { 6 type: 'blueskyMedia', 7 contentComponent: BlueskyMediaCard, 8 createNew: (card) => {}, 9 creationModalComponent: CreateBlueskyMediaCardModal, 10 - sidebarButtonText: 'Bluesky Media' 11 } as CardDefinition & { type: 'blueskyMedia' };
··· 1 import type { CardDefinition } from '../types'; 2 import BlueskyMediaCard from './BlueskyMediaCard.svelte'; 3 import CreateBlueskyMediaCardModal from './CreateBlueskyMediaCardModal.svelte'; 4 + import SidebarItemBlueskyMediaCard from './SidebarItemBlueskyMediaCard.svelte'; 5 6 export const BlueskyMediaCardDefinition = { 7 type: 'blueskyMedia', 8 contentComponent: BlueskyMediaCard, 9 createNew: (card) => {}, 10 creationModalComponent: CreateBlueskyMediaCardModal, 11 + sidebarButtonText: 'Bluesky Media', 12 + sidebarComponent: SidebarItemBlueskyMediaCard 13 } as CardDefinition & { type: 'blueskyMedia' };
-1
src/lib/cards/SectionCard/index.ts
··· 20 card.mobileW = COLUMNS; 21 }, 22 23 - sidebarButtonText: 'Section Headline', 24 defaultColor: 'transparent', 25 maxH: 1, 26 canResize: false
··· 20 card.mobileW = COLUMNS; 21 }, 22 23 defaultColor: 'transparent', 24 maxH: 1, 25 canResize: false
+14 -7
src/lib/cards/SpecialCards/UpdatedBlentos/UpdatedBlentosCard.svelte
··· 2 import type { ContentComponentProps } from '$lib/cards/types'; 3 import { getAdditionalUserData } from '$lib/website/context'; 4 import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; 5 - import { onMount } from 'svelte'; 6 7 let { item }: ContentComponentProps = $props(); 8 9 const data = getAdditionalUserData(); 10 // svelte-ignore state_referenced_locally 11 const profiles = data[item.cardType] as ProfileViewDetailed[]; 12 </script> 13 14 - <div class="h-full flex flex-col"> 15 - <div class="text-2xl font-bold px-4 py-2">Recently updated blentos</div> 16 - <div class="flex grow max-w-full items-center gap-4 overflow-x-scroll overflow-y-hidden px-4"> 17 {#each profiles as profile} 18 <a 19 href="/{profile.handle}" 20 - class="bg-base-100 dark:bg-base-800 hover:bg-base-200 dark:hover:bg-base-700 flex h-52 w-44 min-w-44 flex-col items-center justify-center gap-2 rounded-xl transition-colors duration-150 p-2 accent:bg-accent-200/30 accent:hover:bg-accent-200/50" 21 target="_blank" 22 > 23 - <img src={profile.avatar} class="aspect-square size-28 rounded-full" alt="" /> 24 - <div class="line-clamp-1 text-md font-bold text-center">{profile.displayName || profile.handle}</div> 25 </a> 26 {/each} 27 </div>
··· 2 import type { ContentComponentProps } from '$lib/cards/types'; 3 import { getAdditionalUserData } from '$lib/website/context'; 4 import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; 5 6 let { item }: ContentComponentProps = $props(); 7 8 const data = getAdditionalUserData(); 9 // svelte-ignore state_referenced_locally 10 const profiles = data[item.cardType] as ProfileViewDetailed[]; 11 + 12 + $inspect(profiles); 13 </script> 14 15 + <div class="flex h-full flex-col"> 16 + <div class="px-4 py-2 text-2xl font-bold">Recently updated blentos</div> 17 + <div class="flex max-w-full grow items-center gap-4 overflow-x-scroll overflow-y-hidden px-4"> 18 {#each profiles as profile} 19 <a 20 href="/{profile.handle}" 21 + class="bg-base-100 dark:bg-base-800 hover:bg-base-200 dark:hover:bg-base-700 accent:bg-accent-200/30 accent:hover:bg-accent-200/50 flex h-52 w-44 min-w-44 flex-col items-center justify-center gap-2 rounded-xl p-2 transition-colors duration-150" 22 target="_blank" 23 > 24 + <img 25 + src={profile.avatar} 26 + class="bg-base-200 dark:bg-base-700 accent:bg-accent-300 aspect-square size-28 rounded-full" 27 + alt="" 28 + /> 29 + <div class="text-md line-clamp-1 text-center font-bold"> 30 + {profile.displayName || profile.handle} 31 + </div> 32 </a> 33 {/each} 34 </div>
+45 -25
src/lib/website/EditableWebsite.svelte
··· 779 variant="ghost" 780 class="backdrop-blur-none" 781 onclick={() => { 782 newCard('text'); 783 }} 784 > ··· 788 stroke="currentColor" 789 stroke-linecap="round" 790 stroke-linejoin="round" 791 - stroke-width="1.5" 792 d="m15 16l2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16m-6.303-2h5.606M2 16l4.039-9.69a.5.5 0 0 1 .923 0L11 16m-7.696-3h6.392" 793 /></svg 794 > ··· 809 xmlns="http://www.w3.org/2000/svg" 810 fill="none" 811 viewBox="-2 -2 28 28" 812 - stroke-width="1.5" 813 stroke="currentColor" 814 > 815 <path ··· 837 xmlns="http://www.w3.org/2000/svg" 838 fill="none" 839 viewBox="0 0 24 24" 840 - stroke-width="1.5" 841 stroke="currentColor" 842 class="size-6" 843 > ··· 858 xmlns="http://www.w3.org/2000/svg" 859 fill="none" 860 viewBox="0 0 24 24" 861 - stroke-width="1.5" 862 stroke="currentColor" 863 > 864 <path ··· 870 </Button> 871 872 {#if dev} 873 - <Button 874 - size="iconLg" 875 - variant="ghost" 876 - class="backdrop-blur-none" 877 - onclick={() => { 878 - videoInputRef?.click(); 879 - }} 880 - > 881 - <svg 882 - xmlns="http://www.w3.org/2000/svg" 883 - fill="none" 884 - viewBox="0 0 24 24" 885 - stroke-width="1.5" 886 - stroke="currentColor" 887 > 888 - <path 889 - stroke-linecap="round" 890 - stroke-linejoin="round" 891 - d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" 892 - /> 893 - </svg> 894 - </Button> 895 {/if} 896 897 <Button
··· 779 variant="ghost" 780 class="backdrop-blur-none" 781 onclick={() => { 782 + newCard('section'); 783 + }} 784 + > 785 + <svg 786 + xmlns="http://www.w3.org/2000/svg" 787 + viewBox="0 0 24 24" 788 + fill="none" 789 + stroke="currentColor" 790 + stroke-width="2" 791 + stroke-linecap="round" 792 + stroke-linejoin="round" 793 + ><path d="M6 12h12" /><path d="M6 20V4" /><path d="M18 20V4" /></svg 794 + > 795 + </Button> 796 + 797 + <Button 798 + size="iconLg" 799 + variant="ghost" 800 + class="backdrop-blur-none" 801 + onclick={() => { 802 newCard('text'); 803 }} 804 > ··· 808 stroke="currentColor" 809 stroke-linecap="round" 810 stroke-linejoin="round" 811 + stroke-width="2" 812 d="m15 16l2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16m-6.303-2h5.606M2 16l4.039-9.69a.5.5 0 0 1 .923 0L11 16m-7.696-3h6.392" 813 /></svg 814 > ··· 829 xmlns="http://www.w3.org/2000/svg" 830 fill="none" 831 viewBox="-2 -2 28 28" 832 + stroke-width="2" 833 stroke="currentColor" 834 > 835 <path ··· 857 xmlns="http://www.w3.org/2000/svg" 858 fill="none" 859 viewBox="0 0 24 24" 860 + stroke-width="2" 861 stroke="currentColor" 862 class="size-6" 863 > ··· 878 xmlns="http://www.w3.org/2000/svg" 879 fill="none" 880 viewBox="0 0 24 24" 881 + stroke-width="2" 882 stroke="currentColor" 883 > 884 <path ··· 890 </Button> 891 892 {#if dev} 893 + <Button 894 + size="iconLg" 895 + variant="ghost" 896 + class="backdrop-blur-none" 897 + onclick={() => { 898 + videoInputRef?.click(); 899 + }} 900 > 901 + <svg 902 + xmlns="http://www.w3.org/2000/svg" 903 + fill="none" 904 + viewBox="0 0 24 24" 905 + stroke-width="1.5" 906 + stroke="currentColor" 907 + > 908 + <path 909 + stroke-linecap="round" 910 + stroke-linejoin="round" 911 + d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" 912 + /> 913 + </svg> 914 + </Button> 915 {/if} 916 917 <Button