learn and share notes on atproto (wip) 🦉 malfestio.stormlightlabs.org/
readability solid axum atproto srs
5
fork

Configure Feed

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

feat: refactor router component usage to defer JSX creation

* update serif/display font

+102 -66
+33
docs/solid-carbon.md
··· 53 53 - **Tabs**: Left/Right arrows 54 54 - **Dropdown**: Arrow keys, `Enter` to select, `Escape` to close 55 55 - **Menu**: Arrow keys, `Enter` to select 56 + 57 + ## Common Gotchas 58 + 59 + ### Router Components in Module-Level Constants 60 + 61 + Router components (`<A>`, and hooks like `useNavigate`, `useParams`) must be created during component render, not at module initialization. 62 + 63 + **Problem:** 64 + 65 + ```tsx 66 + // ✗ JSX created when module loads, before Router exists 67 + const config = { 68 + action: <A href="/somewhere">Click me</A> 69 + } 70 + ``` 71 + 72 + **Solution:** 73 + 74 + ```tsx 75 + // ✓ JSX created during render, inside Router context 76 + const getConfig = () => ({ 77 + action: () => <A href="/somewhere">Click me</A> 78 + }) 79 + 80 + const MyComponent = () => { 81 + const config = getConfig(); 82 + return <div>{config.action()}</div> 83 + } 84 + ``` 85 + 86 + **Why:** Module-level JSX executes immediately on import, before `<Router>` establishes its context. Router components need that context to function. 87 + 88 + **Rule:** If storing JSX that contains router components, wrap it in a function to defer creation until render time.
-1
web/package.json
··· 14 14 }, 15 15 "dependencies": { 16 16 "@fontsource-variable/figtree": "^5.2.8", 17 - "@fontsource-variable/source-serif-4": "^5.2.8", 18 17 "@solidjs/meta": "^0.29.4", 19 18 "@solidjs/router": "^0.15.4", 20 19 "@tailwindcss/vite": "^4.1.18",
-8
web/pnpm-lock.yaml
··· 14 14 '@fontsource-variable/figtree': 15 15 specifier: ^5.2.8 16 16 version: 5.2.10 17 - '@fontsource-variable/source-serif-4': 18 - specifier: ^5.2.8 19 - version: 5.2.9 20 17 '@solidjs/meta': 21 18 specifier: ^0.29.4 22 19 version: 0.29.4(solid-js@1.9.10) ··· 483 480 484 481 '@fontsource-variable/figtree@5.2.10': 485 482 resolution: {integrity: sha512-a5Gumbpy3mdd+Yg31g6Qb7CmjYbrfyutJa3bWfP5q8A4GclIOwX7mI+ZuSHsJnw/mHvW6r9oh1AHJcJTIxK4JA==} 486 - 487 - '@fontsource-variable/source-serif-4@5.2.9': 488 - resolution: {integrity: sha512-PPcxjLFk/fS0WHg79pDM2YNvz61kC+oYZ5cWZZyCS0DHpJncmuYOuiZAsvj4tDxlWPBEvxxcRLQQNmSaRbPkqw==} 489 483 490 484 '@humanfs/core@0.19.1': 491 485 resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} ··· 2579 2573 '@exodus/bytes@1.6.0': {} 2580 2574 2581 2575 '@fontsource-variable/figtree@5.2.10': {} 2582 - 2583 - '@fontsource-variable/source-serif-4@5.2.9': {} 2584 2576 2585 2577 '@humanfs/core@0.19.1': {} 2586 2578
web/public/fonts/Mattern-Regular.otf

This is a binary file and will not be displayed.

web/public/fonts/Mattern-Regular.ttf

This is a binary file and will not be displayed.

web/public/og-image.png

This is a binary file and will not be displayed.

+12 -9
web/scripts/generate-og-image.ts
··· 6 6 * Run with: pnpm run generate:og 7 7 */ 8 8 import { Resvg } from "@resvg/resvg-js"; 9 - import { writeFileSync } from "fs"; 9 + import { readFileSync, writeFileSync } from "fs"; 10 10 import { dirname, join } from "path"; 11 11 import satori from "satori"; 12 12 import { fileURLToPath } from "url"; 13 13 14 14 const __dirname = dirname(fileURLToPath(import.meta.url)); 15 15 16 + const FONT_PATH = join(__dirname, "..", "public", "fonts", "Mattern-Regular.ttf"); 16 17 const WIDTH = 1200; 17 18 const HEIGHT = 630; 18 19 const GRID_SIZE = 32; ··· 28 29 */ 29 30 async function fetchFont(family: string, weight: number): Promise<ArrayBuffer> { 30 31 const url = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@${weight}&display=swap`; 31 - 32 32 const cssRes = await fetch(url, { 33 33 headers: { "User-Agent": "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)" }, 34 34 }); 35 - const css = await cssRes.text(); 36 35 36 + const css = await cssRes.text(); 37 37 const fontUrlMatch = css.match(/src: url\(([^)]+)\)/); 38 38 if (!fontUrlMatch) { 39 39 throw new Error(`Could not find font URL for ${family}`); ··· 170 170 children: [{ 171 171 type: "div", 172 172 props: { 173 - style: { fontSize: 96, fontFamily: "Source Serif 4", fontWeight: 500, color: "#ffffff", lineHeight: 1.1 }, 173 + style: { fontSize: 96, fontFamily: "Mattern", fontWeight: 400, color: "#ffffff", lineHeight: 1.1 }, 174 174 children: "Learning on", 175 175 }, 176 176 }, { 177 177 type: "div", 178 178 props: { 179 - style: { fontSize: 96, fontFamily: "Source Serif 4", fontWeight: 500, color: "#737373", lineHeight: 1.1 }, 179 + style: { fontSize: 96, fontFamily: "Mattern", fontWeight: 400, color: "#737373", lineHeight: 1.1 }, 180 180 children: "the AT Protocol.", 181 181 }, 182 182 }], ··· 216 216 217 217 async function main() { 218 218 console.log("Generating OpenGraph image..."); 219 - console.log("Fetching fonts from Google Fonts..."); 219 + console.log("Loading fonts..."); 220 220 221 - const [sourceSerif, figtree] = await Promise.all([fetchFont("Source Serif 4", 500), fetchFont("Figtree", 600)]); 221 + const [matternFont, figtreeFont] = await Promise.all([ 222 + Promise.resolve(readFileSync(FONT_PATH)), 223 + fetchFont("Figtree", 600), 224 + ]); 222 225 223 226 console.log("Rendering SVG..."); 224 227 225 228 const svg = await satori(ogImage, { 226 229 width: WIDTH, 227 230 height: HEIGHT, 228 - fonts: [{ name: "Source Serif 4", data: sourceSerif, weight: 500, style: "normal" }, { 231 + fonts: [{ name: "Mattern", data: matternFont, weight: 400, style: "normal" }, { 229 232 name: "Figtree", 230 - data: figtree, 233 + data: figtreeFont, 231 234 weight: 600, 232 235 style: "normal", 233 236 }],
+18 -18
web/src/App.tsx
··· 18 18 import Review from "$pages/Review"; 19 19 import Search from "$pages/Search"; 20 20 import { Route, Router } from "@solidjs/router"; 21 - import type { Component } from "solid-js"; 21 + import type { Component, ParentComponent } from "solid-js"; 22 22 import { createEffect, createSignal, onMount, Show } from "solid-js"; 23 23 24 - const ProtectedRoute: Component<{ component: Component }> = (props) => { 24 + const ProtectedLayout: ParentComponent = (props) => { 25 25 const [showOnboarding, setShowOnboarding] = createSignal(false); 26 26 27 27 onMount(async () => { ··· 43 43 44 44 return ( 45 45 <Show when={authStore.isAuthenticated()} fallback={<Landing />}> 46 - <AppLayout> 47 - <props.component /> 48 - </AppLayout> 46 + <AppLayout>{props.children}</AppLayout> 49 47 <OnboardingDialog open={showOnboarding()} onComplete={handleOnboardingComplete} /> 50 48 </Show> 51 49 ); ··· 57 55 <Route path="/login" component={Login} /> 58 56 <Route path="/about" component={About} /> 59 57 <Route path="/help" component={Help} /> 60 - <Route path="/" component={() => <ProtectedRoute component={Home} />} /> 61 - <Route path="/decks" component={() => <ProtectedRoute component={Home} />} /> 62 - <Route path="/decks/new" component={() => <ProtectedRoute component={DeckNew} />} /> 63 - <Route path="/notes/new" component={() => <ProtectedRoute component={NoteNew} />} /> 64 - <Route path="/decks/:id" component={() => <ProtectedRoute component={DeckView} />} /> 65 - <Route path="/import" component={() => <ProtectedRoute component={Import} />} /> 66 - <Route path="/import/lecture" component={() => <ProtectedRoute component={LectureImport} />} /> 67 - <Route path="/review" component={() => <ProtectedRoute component={Review} />} /> 68 - <Route path="/review/:deckId" component={() => <ProtectedRoute component={Review} />} /> 69 - <Route path="/feed" component={() => <ProtectedRoute component={Feed} />} /> 70 - <Route path="/search" component={() => <ProtectedRoute component={Search} />} /> 71 - <Route path="/discovery" component={() => <ProtectedRoute component={Discovery} />} /> 72 - <Route path="*" component={() => <ProtectedRoute component={NotFound} />} /> 58 + <Route path="/" component={ProtectedLayout}> 59 + <Route path="/" component={Home} /> 60 + <Route path="/decks" component={Home} /> 61 + <Route path="/decks/new" component={DeckNew} /> 62 + <Route path="/notes/new" component={NoteNew} /> 63 + <Route path="/decks/:id" component={DeckView} /> 64 + <Route path="/import" component={Import} /> 65 + <Route path="/import/lecture" component={LectureImport} /> 66 + <Route path="/review" component={Review} /> 67 + <Route path="/review/:deckId" component={Review} /> 68 + <Route path="/feed" component={Feed} /> 69 + <Route path="/search" component={Search} /> 70 + <Route path="/discovery" component={Discovery} /> 71 + <Route path="*" component={NotFound} /> 72 + </Route> 73 73 </Router> 74 74 ); 75 75 };
-1
web/src/fonts.d.ts
··· 1 1 // CSS-only packages without TypeScript declarations 2 2 declare module "@fontsource-variable/figtree"; 3 - declare module "@fontsource-variable/source-serif-4";
+11 -2
web/src/index.css
··· 1 1 @import "tailwindcss"; 2 2 @plugin "@egoist/tailwindcss-icons"; 3 3 4 + @font-face { 5 + font-family: "Mattern"; 6 + src: url("/fonts/Mattern-Regular.otf") format("opentype"), 7 + url("/fonts/Mattern-Regular.ttf") format("truetype"); 8 + font-weight: 400; 9 + font-style: normal; 10 + font-display: swap; 11 + } 12 + 4 13 @theme { 5 - --font-display: "Source Serif 4 Variable", serif; 6 - --font-body: "Figtree Variable", serif; 14 + --font-display: "Mattern", serif; 15 + --font-body: "Figtree Variable", sans-serif; 7 16 } 8 17 9 18 * {
-1
web/src/index.tsx
··· 1 1 /* @refresh reload */ 2 2 import "@fontsource-variable/figtree"; 3 - import "@fontsource-variable/source-serif-4"; 4 3 import { render } from "solid-js/web"; 5 4 import "./index.css"; 6 5 import App from "./App.tsx";
+12 -11
web/src/pages/Home.tsx
··· 61 61 </Card> 62 62 ); 63 63 64 - type PersonaTip = { title: string; description: string; icon: JSX.Element; action: JSX.Element; tips: string[] }; 64 + type PersonaTip = { title: string; description: string; icon: JSX.Element; action: () => JSX.Element; tips: string[] }; 65 65 66 - const personaTips: Record<Persona, PersonaTip> = { 66 + const getPersonaTips = (): Record<Persona, PersonaTip> => ({ 67 67 learner: { 68 68 title: "Ready to start learning?", 69 69 description: "Find decks from the community or create your own study materials.", 70 70 icon: <span class="i-bi-book text-4xl text-[#0F62FE]" />, 71 - action: ( 71 + action: () => ( 72 72 <div class="flex gap-3 flex-wrap justify-center"> 73 73 <A href="/discovery"> 74 74 <Button variant="secondary">Browse Discovery</Button> ··· 88 88 title: "Create your first deck!", 89 89 description: "Share your knowledge with the community through flashcards.", 90 90 icon: <span class="i-bi-pencil text-4xl text-[#0F62FE]" />, 91 - action: ( 91 + action: () => ( 92 92 <div class="flex gap-3 flex-wrap justify-center"> 93 93 <A href="/decks/new"> 94 94 <Button>Create New Deck</Button> ··· 108 108 title: "Build your collection", 109 109 description: "Discover and organize the best learning content for others.", 110 110 icon: <span class="i-bi-collection text-4xl text-[#0F62FE]" />, 111 - action: ( 111 + action: () => ( 112 112 <div class="flex gap-3 flex-wrap justify-center"> 113 113 <A href="/feed"> 114 114 <Button variant="secondary">View Feed</Button> ··· 124 124 "Use tags to organize by topic", 125 125 ], 126 126 }, 127 - }; 127 + }); 128 128 129 - const defaultTip: PersonaTip = { 129 + const getDefaultTip = (): PersonaTip => ({ 130 130 title: "No decks found", 131 131 description: "Create your first deck to get started with spaced repetition learning.", 132 132 icon: <span class="i-bi-collection text-4xl text-[#525252]" />, 133 - action: ( 133 + action: () => ( 134 134 <A href="/decks/new"> 135 135 <Button>Create Your First Deck</Button> 136 136 </A> 137 137 ), 138 138 tips: [], 139 - }; 139 + }); 140 140 141 141 const Home: Component = () => { 142 142 const [decks] = createResource(async () => { ··· 146 146 147 147 const currentTip = createMemo(() => { 148 148 const persona = prefStore.persona(); 149 - return persona ? personaTips[persona] : defaultTip; 149 + const tips = getPersonaTips(); 150 + return persona ? tips[persona] : getDefaultTip(); 150 151 }); 151 152 152 153 return ( ··· 181 182 title={currentTip().title} 182 183 description={currentTip().description} 183 184 icon={currentTip().icon} 184 - action={currentTip().action} /> 185 + action={currentTip().action()} /> 185 186 <Show when={currentTip().tips.length > 0}> 186 187 <div class="mt-8 p-6 bg-[#1E1E1E] rounded-lg border border-[#262626] max-w-lg mx-auto"> 187 188 <h4 class="text-sm font-medium text-[#F4F4F4] mb-3">Quick Tips</h4>
+16 -15
web/src/pages/Landing.tsx
··· 1 1 import { Footer } from "$components/layout/Footer"; 2 - import { A } from "@solidjs/router"; 3 2 import type { Component, JSX } from "solid-js"; 4 3 import { For } from "solid-js"; 5 4 import { Motion } from "solid-motionone"; ··· 7 6 const features = [{ 8 7 title: "Flashcards", 9 8 desc: "Built-in spaced repetition system (SRS) ensuring you review the right material at the right time.", 10 - icon: <span class="i-bi-card-text text-4xl" />, 9 + icon: () => <span class="i-bi-card-text text-4xl" />, 11 10 }, { 12 11 title: "Linked Notes", 13 12 desc: "Connect concepts with bidirectional links. Build a knowledge graph that grows with your understanding.", 14 - icon: <span class="i-bi-link-45deg text-4xl" />, 13 + icon: () => <span class="i-bi-link-45deg text-4xl" />, 15 14 }, { 16 15 title: "Lectures & Articles", 17 16 desc: "Import content directly. Highlight, annotate, and turn key insights into flashcards instantly.", 18 - icon: <span class="i-bi-book text-4xl" />, 17 + icon: () => <span class="i-bi-book text-4xl" />, 19 18 }, { 20 19 title: "Social Learning", 21 20 desc: "Publish your decks, follow curators, and fork existing content to improve it for everyone.", 22 - icon: <span class="i-bi-people text-4xl" />, 21 + icon: () => <span class="i-bi-people text-4xl" />, 23 22 }, { 24 23 title: "Local-First", 25 24 desc: "Your data lives on your device. Offline-first architecture with ATProto for decentralized sync.", 26 - icon: <span class="i-bi-hdd text-4xl" />, 25 + icon: () => <span class="i-bi-hdd text-4xl" />, 27 26 }, { 28 27 title: "Open Source", 29 28 desc: "Validates knowledge, not proprietary locks. Inspect the code, extend the schema, own the platform.", 30 - icon: ( 29 + icon: () => ( 31 30 <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 25 25" class="text-4xl"> 32 31 <path 33 32 fill="currentColor" ··· 41 40 desc: "Review with spaced repetition — the right card at the right time.", 42 41 }, { title: "Share", desc: "Publish to the AT Protocol network and discover community content." }]; 43 42 44 - const Feature: Component<{ title: string; desc: string; icon: JSX.Element }> = (props) => ( 43 + const Feature: Component<{ title: string; desc: string; Icon: Component }> = (props) => ( 45 44 <div class="border border-neutral-800 p-6 hover:border-blue-600 transition-colors group h-full bg-neutral-900/50 backdrop-blur-sm"> 46 - <div class="w-10 h-10 mb-4 text-blue-500 group-hover:text-blue-400 transition-colors">{props.icon}</div> 45 + <div class="w-10 h-10 mb-4 text-blue-500 group-hover:text-blue-400 transition-colors"> 46 + <props.Icon /> 47 + </div> 47 48 <h3 class="text-xl text-white mb-2 group-hover:text-blue-400 transition-colors">{props.title}</h3> 48 49 <p class="text-neutral-400 font-light leading-relaxed">{props.desc}</p> 49 50 </div> ··· 77 78 <div class="min-h-screen bg-black text-white font-sans selection:bg-blue-500/30"> 78 79 <header class="border-b border-neutral-900 sticky top-0 bg-black/80 backdrop-blur-md z-50"> 79 80 <div class="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between"> 80 - <A href="/" class="font-bold tracking-tight text-xl hover:text-blue-400 transition-colors">Malfestio</A> 81 + <a href="/" class="font-bold tracking-tight text-xl hover:text-blue-400 transition-colors">Malfestio</a> 81 82 <div class="flex items-center gap-6"> 82 - <A href="/about" class="text-sm font-medium text-neutral-400 hover:text-white transition-colors">About</A> 83 - <A href="/login" class="text-sm font-medium text-neutral-400 hover:text-white transition-colors">Log in</A> 83 + <a href="/about" class="text-sm font-medium text-neutral-400 hover:text-white transition-colors">About</a> 84 + <a href="/login" class="text-sm font-medium text-neutral-400 hover:text-white transition-colors">Log in</a> 84 85 </div> 85 86 </div> 86 87 </header> ··· 151 152 animate={{ opacity: 1, y: 0 }} 152 153 transition={{ duration: 0.6, delay: 0.2 }} 153 154 class="flex gap-4"> 154 - <A 155 + <a 155 156 href="/login" 156 157 class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-4 font-medium text-lg transition-colors inline-flex items-center gap-2"> 157 158 Get Started 158 159 <span class="text-xl">→</span> 159 - </A> 160 + </a> 160 161 </Motion.div> 161 162 </div> 162 163 </div> 163 164 </section> 164 165 <section class="max-w-7xl mx-auto px-6 py-24"> 165 166 <div class="grid grid-cols-1 md:grid-cols-3 gap-6"> 166 - <For each={features}>{(f) => <Feature title={f.title} desc={f.desc} icon={f.icon} />}</For> 167 + <For each={features}>{(f) => <Feature title={f.title} desc={f.desc} Icon={f.icon} />}</For> 167 168 </div> 168 169 </section> 169 170 <section class="border-t border-neutral-900 py-24 relative">