Repository: iammatthias/.com Branch: main Commit: e81920725f27 Files: 56 Total size: 173.4 KB Directory structure: gitextract_gr3u_la6/ ├── .cursor/ │ └── mcp.json ├── .gitignore ├── .gitmodules ├── .vscode/ │ ├── extensions.json │ └── launch.json ├── README.md ├── astro.config.mjs ├── package.json ├── public/ │ ├── .well-known/ │ │ └── atproto-did │ └── rss.xml.xsl ├── src/ │ ├── components/ │ │ ├── BlackHole/ │ │ │ ├── index.tsx │ │ │ └── shaders.ts │ │ ├── Bluesky/ │ │ │ ├── Bluesky.css │ │ │ └── index.tsx │ │ ├── Farcaster/ │ │ │ ├── Farcaster.css │ │ │ └── index.tsx │ │ ├── OnchainAnalytics/ │ │ │ ├── OnchainAnalytics.css │ │ │ └── index.tsx │ │ ├── RecentGlass/ │ │ │ ├── RecentGlass.css │ │ │ └── index.tsx │ │ ├── SocialFeeds/ │ │ │ ├── SocialFeeds.css │ │ │ └── index.tsx │ │ ├── footer.astro │ │ ├── homepage/ │ │ │ ├── heroSection.astro │ │ │ ├── recentContent.astro │ │ │ └── recentFeeds.astro │ │ ├── nav.astro │ │ └── sidebar/ │ │ └── index.astro │ ├── content.config.ts │ ├── layouts/ │ │ └── defaultLayouts.astro │ ├── lib/ │ │ ├── github-loader.ts │ │ ├── og-renderer.ts │ │ ├── tags-loader.ts │ │ └── viemProvider.ts │ ├── pages/ │ │ ├── 404.astro │ │ ├── about.astro │ │ ├── api/ │ │ │ ├── bluesky.json.ts │ │ │ ├── farcaster.json.ts │ │ │ └── glass.json.ts │ │ ├── content/ │ │ │ ├── [collection]/ │ │ │ │ ├── [slug].astro │ │ │ │ ├── index.astro │ │ │ │ └── rss.xml.ts │ │ │ └── index.astro │ │ ├── index.astro │ │ ├── now.astro │ │ ├── onchain-analytics/ │ │ │ ├── [hash]/ │ │ │ │ └── index.astro │ │ │ └── index.astro │ │ ├── resume.astro │ │ ├── robots.txt.ts │ │ ├── rss.xml.ts │ │ └── tags/ │ │ ├── [tag]/ │ │ │ └── index.astro │ │ └── index.astro │ └── styles/ │ ├── globals.css │ └── reset.css ├── tsconfig.json └── wrangler.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cursor/mcp.json ================================================ { "mcpServers": { "Astro docs": { "type": "http", "url": "https://mcp.docs.astro.build/mcp" } } } ================================================ FILE: .gitignore ================================================ # build output dist/ # generated types .astro/ # dependencies node_modules/ # logs npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # environment variables .env .env.production .dev.vars # macOS-specific files .DS_Store # jetbrains setting folder .idea/ .mastra .vercel # temporary exclude for wrangler worker/ .wrangler/ ================================================ FILE: .gitmodules ================================================ [submodule "worker-og"] path = worker-og url = https://github.com/iammatthias/og.git ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": ["astro-build.astro-vscode"], "unwantedRecommendations": [] } ================================================ FILE: .vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "command": "./node_modules/.bin/astro dev", "name": "Development server", "request": "launch", "type": "node-terminal" } ] } ================================================ FILE: README.md ================================================ ``` `7MMF' MM MM ,6"Yb. `7MMpMMMb.pMMMb. MM 8) MM MM MM MM MM ,pm9MM MM MM MM MM 8M MM MM MM MM .JMML. `Moo9^Yo..JMML JMML JMML. ,, ,, `7MMM. ,MMF' mm mm `7MM db MMMb dPMM MM MM MM M YM ,M MM ,6"Yb.mmMMmm mmMMmm MMpMMMb. `7MM ,6"Yb. ,pP"Ybd M Mb M' MM 8) MM MM MM MM MM MM 8) MM 8I `" M YM.P' MM ,pm9MM MM MM MM MM MM ,pm9MM `YMMMa. M `YM' MM 8M MM MM MM MM MM MM 8M MM L. I8 .JML. `' .JMML.`Moo9^Yo.`Mbmo `Mbmo.JMML JMML..JMML.`Moo9^Yo.M9mmmP' ``` ### hi This is the latest version of my site. It is built with [Astro](https://astro.build/), and the content is authored in [Obsidian](https://obsidian.md/) and stored in a private repo. Images hosted on Pinata IPFS. The site is hosted on [Vercel](https://vercel.com/), and I'm using [PostHog](https://posthog.com/) for some basic analytics. > The code is provided as-is, and I'm not planning to provide support for this setup. Feel free to use it as inspiration for your own projects. ### built with - [Astro](https://astro.build/) - [Obsidian](https://obsidian.md/) - [Pinata](https://www.pinata.cloud/) - [Cloudflare](https://www.cloudflare.com/) ================================================ FILE: astro.config.mjs ================================================ // @ts-check import { defineConfig } from "astro/config"; import fs from 'node:fs'; import path from 'node:path'; import react from "@astrojs/react"; import rehypeUnwrapImages from "rehype-unwrap-images"; import sitemap from "@astrojs/sitemap"; import cloudflare from "@astrojs/cloudflare"; /** * @param {string[]} extensions * @returns {import('vite').Plugin} */ function rawFonts(extensions) { return { name: 'vite-plugin-raw-fonts', enforce: /** @type {const} */ ('pre'), resolveId(id, importer) { if (extensions.some(ext => id.includes(ext))) { if (id.startsWith('.')) { const resolvedPath = path.resolve(path.dirname(importer || ''), id); return resolvedPath; } return id; } }, load(id) { if (extensions.some(ext => id.includes(ext))) { try { const buffer = fs.readFileSync(id); return `export default new Uint8Array([${Array.from(buffer).join(',')}]);`; } catch (error) { const err = /** @type {Error} */ (error); console.error('Error loading font:', err.message); throw error; } } } }; } // https://astro.build/config export default defineConfig({ site: "https://iammatthias.com", integrations: [react(), sitemap()], markdown: { rehypePlugins: [rehypeUnwrapImages], }, redirects: { "/art": { status: 302, destination: "/content/art", }, "/open-source": { status: 302, destination: "/content/open-source", }, "/posts": { status: 302, destination: "/content/posts", }, "/recipes": { status: 302, destination: "/content/recipes", }, "/post/1563778800000": { status: 302, destination: "/content/notes/1563778800000-flatframe", }, "/post/1587970800000": { status: 302, destination: "/content/notes/1587970800000-sourdough", }, "/post/1687071600000": { status: 302, destination: "/content/notes/1687071600000-feels-like-summer", }, "/post/1681369200000": { status: 302, destination: "/content/posts/1681369200000-ai-legion-on-bluesky", }, "/post/1670659200001": { status: 302, destination: "/content/posts/1670659200001-obsidian-as-a-cms", }, "/post/1691436904927": { status: 302, destination: "/content/posts/1691436904927-deploy-html-on-replit", }, "/post/1699332127006": { status: 302, destination: "/content/posts/1699332127006-revisiting-obsidian-as-a-cms", }, }, adapter: cloudflare({ prerenderEnvironment: 'node', }), vite: { plugins: [rawFonts(['.ttf'])], assetsInclude: ['**/*.wasm'], resolve: { alias: { '@src': path.resolve('./src'), '@styles': path.resolve('./src/styles'), '@components': path.resolve('./src/components'), '@layouts': path.resolve('./src/layouts'), '@pages': path.resolve('./src/pages'), '@mastra': path.resolve('./src/mastra'), '@actions': path.resolve('./src/actions'), '@lib': path.resolve('./src/lib'), }, }, ssr: { external: ["buffer", "path", "fs"].map((i) => `node:${i}`), noExternal: ['@cf-wasm/resvg'], }, }, }); ================================================ FILE: package.json ================================================ { "name": "com", "type": "module", "version": "0.0.1", "scripts": { "dev": "astro dev", "build": "astro build", "preview": "astro preview", "astro": "astro" }, "dependencies": { "@astrojs/cloudflare": "^13.1.10", "@astrojs/react": "^5.0.3", "@astrojs/rss": "^4.0.18", "@astrojs/sitemap": "^3.7.2", "@cf-wasm/resvg": "^0.3.3", "@cloudflare/agents": "^0.0.16", "@modelcontextprotocol/sdk": "^1.29.0", "@noble/hashes": "^2.2.0", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "astro": "^6.1.8", "pinata": "^2.5.5", "react": "^19.2.5", "react-dom": "^19.2.5", "react-plock": "^3.6.1", "rehype-unwrap-images": "^1.0.0", "satori": "^0.26.0", "satori-html": "^0.3.2", "three": "^0.184.0", "viem": "^2.48.1", "zod": "^4" }, "devDependencies": { "wrangler": "^4.84.0" } } ================================================ FILE: public/.well-known/atproto-did ================================================ did:plc:p5xem22ammiafn5kxonaksfa ================================================ FILE: public/rss.xml.xsl ================================================ <xsl:value-of select="title"/> - RSS Feed
================================================ FILE: src/components/BlackHole/index.tsx ================================================ import { useRef, useMemo, useEffect } from "react"; import { Canvas, useFrame, useThree } from "@react-three/fiber"; import * as THREE from "three"; import { vertexShader, fragmentShader } from "./shaders"; // Seeded random number generator class SeededRNG { seed: number; constructor(seed: number) { this.seed = seed % 2147483647; if (this.seed <= 0) this.seed += 2147483646; } next() { this.seed = (this.seed * 16807) % 2147483647; return (this.seed - 1) / 2147483646; } } // Generate procedural starfield texture function generateStarTexture(seed: number, size: number = 1024): THREE.DataTexture { const data = new Uint8Array(size * size * 4); const rng = new SeededRNG(seed); // Fill with dark background for (let i = 0; i < size * size * 4; i += 4) { data[i] = 2; data[i + 1] = 2; data[i + 2] = 5; data[i + 3] = 255; } // Add stars - scale count based on texture size const starCount = Math.floor((size / 1024) * (size / 1024) * 4000); for (let i = 0; i < starCount; i++) { const x = Math.floor(rng.next() * size); const y = Math.floor(rng.next() * size); const idx = (y * size + x) * 4; const bright = 200 + Math.floor(rng.next() * 55); const tint = rng.next(); if (tint > 0.95) { // Blue stars data[idx] = bright * 0.8; data[idx + 1] = bright * 0.9; data[idx + 2] = bright; } else if (tint > 0.9) { // Orange stars data[idx] = bright; data[idx + 1] = bright * 0.8; data[idx + 2] = bright * 0.6; } else { // White stars data[idx] = bright; data[idx + 1] = bright; data[idx + 2] = bright; } data[idx + 3] = 255; } const texture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat); texture.needsUpdate = true; texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; return texture; } function BlackHoleShader({ seed, iterations }: { seed: number; iterations: number }) { const meshRef = useRef(null); const { viewport, size } = useThree(); // Reuse geometry across renders const geometry = useMemo(() => { return new THREE.PlaneGeometry(1, 1); }, []); const material = useMemo(() => { // Scale texture size based on iterations for memory optimization const textureSize = Math.min(1024, Math.max(256, Math.floor(iterations * 4))); const texture = generateStarTexture(seed, textureSize); // Enable mipmaps for better memory usage texture.generateMipmaps = true; texture.minFilter = THREE.LinearMipmapLinearFilter; return new THREE.ShaderMaterial({ vertexShader, fragmentShader, uniforms: { uSpaceTexture: { value: texture }, uResolution: { value: new THREE.Vector2(size.width, size.height) }, uTime: { value: 0 }, uMaxIterations: { value: iterations }, }, }); }, [seed, iterations]); useEffect(() => { const handleResize = () => { if (material.uniforms.uResolution) { material.uniforms.uResolution.value.set(window.innerWidth, window.innerHeight); } }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [material]); useEffect(() => { return () => { material.uniforms.uSpaceTexture.value?.dispose(); material.dispose(); geometry.dispose(); }; }, [material, geometry]); useFrame((state) => { if (material.uniforms.uTime) { material.uniforms.uTime.value = state.clock.elapsedTime; } }); return ( ); } export default function BlackHole({ seed = 404 }: { seed?: number }) { // Derive iterations from seed (50-250 range) const iterations = useMemo(() => { const normalized = (seed % 200) + 50; // Maps to 50-250 range return normalized; }, [seed]); // Scale down DPR for high iteration counts to save memory/performance const dpr = useMemo((): [number, number] => { if (iterations > 200) return [0.5, 0.75]; if (iterations > 150) return [0.75, 1]; return [1, 1.25]; }, [iterations]); return (
); } ================================================ FILE: src/components/BlackHole/shaders.ts ================================================ export const vertexShader = ` varying vec2 vUv; void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); vUv = uv; } `; export const fragmentShader = ` varying vec2 vUv; uniform sampler2D uSpaceTexture; uniform vec2 uResolution; uniform float uTime; uniform int uMaxIterations; #define STEP_SIZE 0.1 #define PI 3.1415926535897932384626433832795 #define TAU 6.283185307179586476925286766559 vec3 camPos = vec3(0, 1, -7); vec3 blackholePos = vec3(0, 0, 0); float innerDiskRadius = 2.0; float outerDiskRadius = 5.0; float diskTwist = 10.0; float flowRate = 0.6; // Noise functions for disk texture float hash(float n) { return fract(sin(n) * 753.5453123); } float noise(vec3 x) { vec3 p = floor(x); vec3 f = fract(x); f = f * f * (3.0 - 2.0 * f); float n = p.x + p.y * 157.0 + 113.0 * p.z; return mix(mix(mix(hash(n + 0.0), hash(n + 1.0), f.x), mix(hash(n + 157.0), hash(n + 158.0), f.x), f.y), mix(mix(hash(n + 113.0), hash(n + 114.0), f.x), mix(hash(n + 270.0), hash(n + 271.0), f.x), f.y), f.z); } float fbm(vec3 pos, const int numOctaves, const float iterScale, const float detail, const float weight) { float mul = weight; float add = 1.0 - 0.5 * mul; float t = noise(pos) * mul + add; for (int i = 1; i < numOctaves; ++i) { pos *= iterScale; mul = exp2(log2(weight) - float(i) / detail); add = 1.0 - 0.5 * mul; t *= noise(pos) * mul + add; } return t; } vec4 raytrace(vec3 rayDir, vec3 rayPos) { vec4 color = vec4(0, 0, 0, 1); float h2 = pow(length(cross(rayPos, rayDir)), 2.0); for (int i = 0; i < uMaxIterations; i++) { float dist = length(rayPos - blackholePos); // Event horizon check if (dist < 1.0) { return vec4(0, 0, 0, 1); } // Schwarzschild metric rayDir += -1.5 * h2 * rayPos / pow(pow(dist, 2.0), 2.5) * STEP_SIZE; vec3 steppedRayPos = rayPos + rayDir * STEP_SIZE; // Varying depth for accretion disk float depth = pow(STEP_SIZE, 3.0) + fbm(rayPos, 5, 10.0, 1.8, 10.5) * 0.05; // Accretion disk check if (dist > innerDiskRadius && dist < outerDiskRadius && rayPos.y * steppedRayPos.y < depth) { // Disk UV calculation float deltaDiskRadius = outerDiskRadius - innerDiskRadius; float diskDist = dist - innerDiskRadius; vec3 uvw = vec3( (atan(steppedRayPos.z, abs(steppedRayPos.x)) / TAU) - (diskTwist / sqrt(dist)), pow(diskDist / deltaDiskRadius, 2.0) + ((flowRate / TAU) / deltaDiskRadius), steppedRayPos.y * 0.5 + 0.5 ) / 2.0; // Disk density with texture float diskDensity = 1.0 - length(steppedRayPos / vec3(outerDiskRadius, 1.0, outerDiskRadius)); diskDensity *= smoothstep(innerDiskRadius, innerDiskRadius + 1.0, dist); float densityVariation = fbm(2.0 * uvw, 5, 2.0, 1.0, 1.0); diskDensity *= inversesqrt(dist) * densityVariation; float opticalDepth = STEP_SIZE * 50.0 * diskDensity; // Doppler shift vec3 shiftVector = 0.6 * cross(normalize(steppedRayPos), vec3(0.0, 1.0, 0.0)); float velocity = dot(rayDir, shiftVector); float dopplerShift = sqrt((1.0 - velocity) / (1.0 + velocity)); // Gravitational shift float gravitationalShift = sqrt((1.0 - 2.0 / dist) / (1.0 - 2.0 / length(camPos))); return vec4(vec3(1) * dopplerShift * gravitationalShift * opticalDepth, 1.0); } rayPos = steppedRayPos; } // Sample background texture // Convert ray direction to spherical coordinates for texture sampling float theta = atan(rayDir.z, rayDir.x); float phi = asin(rayDir.y); vec2 texCoord = vec2( theta / TAU + 0.5, phi / PI + 0.5 ); color = texture2D(uSpaceTexture, texCoord); return color; } void main() { vec2 uv = (vUv - 0.5) * 2.0 * vec2(uResolution.x / uResolution.y, 1); vec3 rayDir = normalize(vec3(uv, 1)); vec3 rayPos = camPos; gl_FragColor = raytrace(rayDir, rayPos); } `; ================================================ FILE: src/components/Bluesky/Bluesky.css ================================================ .bsky-container { display: flex; flex-direction: column; gap: 1rem; } .bsky-feed { display: flex; flex-direction: column; gap: 1rem; } .bsky-post { border: 1px solid var(--color-border); padding: 1rem; display: flex; flex-direction: column; gap: 0.75rem; } .bsky-post-link { text-decoration: none; color: inherit; display: flex; flex-direction: column; gap: 0.75rem; } .bsky-post-header { display: flex; align-items: center; gap: 0.5rem; } .bsky-avatar { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; } .bsky-author { display: flex; flex-direction: column; flex: 1; min-width: 0; } .bsky-display-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .bsky-handle { color: var(--color-muted); font-size: 0.875rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .bsky-time { color: var(--color-muted); font-size: 0.875rem; white-space: nowrap; } .bsky-text { margin: 0; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; } /* Image grid layouts */ .bsky-images { display: grid; gap: 0.25rem; border-radius: 0.5rem; overflow: hidden; } .bsky-images-1 { grid-template-columns: 1fr; } .bsky-images-2 { grid-template-columns: 1fr 1fr; } .bsky-images-3 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; } .bsky-images-3 .bsky-image:first-child { grid-row: span 2; } .bsky-images-4 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; } .bsky-image { display: block; overflow: hidden; } .bsky-image img { width: 100%; height: 100%; object-fit: cover; transition: opacity 0.2s ease; } .bsky-image:hover img { opacity: 0.9; } /* Video embed */ .bsky-video { border-radius: 0.5rem; overflow: hidden; } .bsky-video video { width: 100%; display: block; } .bsky-video-alt { display: block; padding: 0.5rem; font-size: 0.875rem; color: var(--color-muted); background: var(--color-background); } /* External link embed */ .bsky-external { display: flex; flex-direction: column; border: 1px solid var(--color-border); border-radius: 0.5rem; overflow: hidden; text-decoration: none; color: inherit; transition: border-color 0.2s ease; } .bsky-external:hover { border-color: var(--color-muted); } .bsky-external-thumb { width: 100%; height: auto; max-height: 200px; object-fit: cover; } .bsky-external-content { padding: 0.75rem; display: flex; flex-direction: column; gap: 0.25rem; } .bsky-external-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .bsky-external-description { font-size: 0.875rem; color: var(--color-muted); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .bsky-external-uri { font-size: 0.75rem; color: var(--color-muted); } /* Quote embed */ .bsky-quote { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem; border: 1px solid var(--color-border); border-radius: 0.5rem; text-decoration: none; color: inherit; transition: border-color 0.2s ease; } .bsky-quote:hover { border-color: var(--color-muted); } .bsky-quote-header { display: flex; align-items: center; gap: 0.5rem; } .bsky-quote-avatar { width: 20px; height: 20px; border-radius: 50%; object-fit: cover; } .bsky-quote-author { display: flex; gap: 0.25rem; font-size: 0.875rem; overflow: hidden; } .bsky-quote-handle { color: var(--color-muted); } .bsky-quote-text { margin: 0; font-size: 0.875rem; color: var(--color-muted); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } /* Quote label */ .bsky-quote-label { font-size: 0.875rem; color: var(--color-muted); } /* Loading/error/empty states */ .bsky-loading, .bsky-error, .bsky-empty { text-align: center; padding: 2rem; color: var(--color-muted); } .bsky-error { color: var(--color-error, #e74c3c); } /* View all link */ .bsky-view-all { text-align: center; margin-top: 0.5rem; } .bsky-view-all a { text-decoration: none; color: inherit; font-weight: 500; } .bsky-view-all a:hover { text-decoration: underline; } /* Responsive */ @media (max-width: 768px) { .bsky-post { padding: 0.75rem; } .bsky-avatar { width: 32px; height: 32px; } .bsky-images-3 .bsky-image:first-child { grid-row: span 1; } .bsky-images-3 { grid-template-columns: 1fr; grid-template-rows: auto; } } ================================================ FILE: src/components/Bluesky/index.tsx ================================================ import { useEffect, useState } from "react"; import "./Bluesky.css"; // Types matching the API response interface NormalizedImage { type: "image"; thumb: string; fullsize: string; alt: string; aspectRatio?: { width: number; height: number }; mimeType: string; } interface NormalizedVideo { type: "video"; url: string; alt?: string; aspectRatio?: { width: number; height: number }; mimeType: string; } interface NormalizedExternal { type: "external"; uri: string; title: string; description: string; thumb?: string; } interface NormalizedQuote { type: "quote"; uri: string; cid: string; } type NormalizedEmbed = | NormalizedImage | NormalizedVideo | NormalizedExternal | NormalizedQuote; interface BlueskyPost { uri: string; cid: string; text: string; createdAt: string; embeds: NormalizedEmbed[]; postUrl: string; } function formatDate(dateString: string): string { const date = new Date(dateString); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return "just now"; if (diffMins < 60) return `${diffMins}m`; if (diffHours < 24) return `${diffHours}h`; if (diffDays < 7) return `${diffDays}d`; return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined, }); } function ImageEmbed({ embed }: { embed: NormalizedImage }) { return ( {embed.alt} ); } function VideoEmbed({ embed }: { embed: NormalizedVideo }) { return (
{embed.alt && {embed.alt}}
); } function ExternalEmbed({ embed }: { embed: NormalizedExternal }) { return ( {embed.thumb && ( )}
{embed.title} {embed.description} {new URL(embed.uri).hostname}
); } function QuoteEmbed({ embed }: { embed: NormalizedQuote }) { // Extract handle/postId from URI: at://did:plc:xxx/app.bsky.feed.post/postid const uriParts = embed.uri.split("/"); const postId = uriParts[uriParts.length - 1]; const did = uriParts[2]; return ( Quoted post ); } function EmbedRenderer({ embed }: { embed: NormalizedEmbed }) { switch (embed.type) { case "image": return ; case "video": return ; case "external": return ; case "quote": return ; default: return null; } } function PostCard({ post }: { post: BlueskyPost }) { // Group images together for grid layout const images = post.embeds.filter( (e): e is NormalizedImage => e.type === "image", ); const otherEmbeds = post.embeds.filter((e) => e.type !== "image"); return ( ); } interface BlueskyFeedProps { limit?: number; } export default function BlueskyFeed({ limit = 10 }: BlueskyFeedProps) { const [posts, setPosts] = useState([]); const [isLoading, setIsLoading] = useState(true); const [errorMsg, setErrorMsg] = useState(null); useEffect(() => { async function fetchPosts() { setIsLoading(true); setErrorMsg(null); try { const response = await fetch( `/api/bluesky.json?limit=${limit}&_=${Date.now()}`, ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data: BlueskyPost[] = await response.json(); console.log( "Bluesky posts received:", data.map((p) => ({ text: p.text.slice(0, 30), createdAt: p.createdAt, })), ); setPosts(data); } catch (error) { const msg = "Error fetching Bluesky posts: " + (error instanceof Error ? error.message : String(error)); setErrorMsg(msg); console.error(msg); } finally { setIsLoading(false); } } fetchPosts(); }, [limit]); if (isLoading) { return (
Loading posts from Bluesky...
); } if (errorMsg) { return (
{errorMsg}
); } if (posts.length === 0) { return (
No posts found.
); } return (
{posts.map((post) => ( ))}
); } ================================================ FILE: src/components/Farcaster/Farcaster.css ================================================ .fc-container { display: flex; flex-direction: column; gap: 1rem; } .fc-feed { display: flex; flex-direction: column; gap: 1rem; } .fc-post { border: 1px solid var(--color-border); padding: 1rem; display: flex; flex-direction: column; gap: 0.75rem; } .fc-post-link { text-decoration: none; color: inherit; display: flex; flex-direction: column; gap: 0.5rem; } .fc-time { color: var(--color-muted); font-size: 0.875rem; } .fc-text { margin: 0; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; } /* Image grid layouts */ .fc-images { display: grid; gap: 0.25rem; border-radius: 0.5rem; overflow: hidden; } .fc-images-1 { grid-template-columns: 1fr; } .fc-images-2 { grid-template-columns: 1fr 1fr; } .fc-images-3 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; } .fc-images-3 .fc-image:first-child { grid-row: span 2; } .fc-images-4 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; } .fc-image { display: block; overflow: hidden; } .fc-image img { width: 100%; height: 100%; object-fit: cover; transition: opacity 0.2s ease; } .fc-image:hover img { opacity: 0.9; } /* Link embed */ .fc-link { display: inline-block; padding: 0.5rem 0.75rem; border: 1px solid var(--color-border); border-radius: 0.25rem; text-decoration: none; color: var(--color-muted); font-size: 0.875rem; transition: border-color 0.2s ease; } .fc-link:hover { border-color: var(--color-foreground); } /* Quote embed */ .fc-quote { display: flex; padding: 0.75rem; border: 1px solid var(--color-border); border-radius: 0.5rem; text-decoration: none; color: inherit; transition: border-color 0.2s ease; } .fc-quote:hover { border-color: var(--color-muted); } .fc-quote-label { font-size: 0.875rem; color: var(--color-muted); } /* Loading/error states */ .fc-loading, .fc-error { text-align: center; padding: 2rem; color: var(--color-muted); } .fc-error { color: var(--color-error, #e74c3c); } /* View all link */ .fc-view-all { text-align: center; margin-top: 0.5rem; } .fc-view-all a { text-decoration: none; color: inherit; font-weight: 500; } .fc-view-all a:hover { text-decoration: underline; } /* Responsive */ @media (max-width: 768px) { .fc-post { padding: 0.75rem; } .fc-images-3 .fc-image:first-child { grid-row: span 1; } .fc-images-3 { grid-template-columns: 1fr; grid-template-rows: auto; } } ================================================ FILE: src/components/Farcaster/index.tsx ================================================ import { useEffect, useState } from "react"; import "./Farcaster.css"; interface NormalizedEmbed { type: "url" | "cast"; url?: string; castFid?: number; castHash?: string; } interface FarcasterPost { hash: string; text: string; createdAt: string; embeds: NormalizedEmbed[]; postUrl: string; } function formatDate(dateString: string): string { const date = new Date(dateString); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return "just now"; if (diffMins < 60) return `${diffMins}m`; if (diffHours < 24) return `${diffHours}h`; if (diffDays < 7) return `${diffDays}d`; return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined, }); } function isImageUrl(url: string): boolean { return /\.(jpg|jpeg|png|gif|webp)$/i.test(url); } function UrlEmbed({ url }: { url: string }) { if (isImageUrl(url)) { return ( ); } return ( {new URL(url).hostname} ); } function PostCard({ post }: { post: FarcasterPost }) { const imageEmbeds = post.embeds.filter((e) => e.type === "url" && e.url && isImageUrl(e.url)); const otherEmbeds = post.embeds.filter((e) => !(e.type === "url" && e.url && isImageUrl(e.url))); return (
{post.text &&

{post.text}

}
{imageEmbeds.length > 0 && (
{imageEmbeds.map((embed, i) => ( ))}
)} {otherEmbeds.map((embed, i) => { if (embed.type === "url" && embed.url) { return ; } if (embed.type === "cast" && embed.castHash) { const hashHex = embed.castHash.startsWith("0x") ? embed.castHash.slice(2) : embed.castHash; return ( Quoted cast ); } return null; })}
); } interface FarcasterFeedProps { limit?: number; onDataLoaded?: (posts: FarcasterPost[]) => void; } export default function FarcasterFeed({ limit = 10, onDataLoaded }: FarcasterFeedProps) { const [posts, setPosts] = useState([]); const [isLoading, setIsLoading] = useState(true); const [errorMsg, setErrorMsg] = useState(null); useEffect(() => { async function fetchPosts() { setIsLoading(true); setErrorMsg(null); try { const response = await fetch(`/api/farcaster.json?limit=${limit}&_=${Date.now()}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data: FarcasterPost[] = await response.json(); setPosts(data); onDataLoaded?.(data); } catch (error) { const msg = "Error fetching Farcaster posts: " + (error instanceof Error ? error.message : String(error)); setErrorMsg(msg); console.error(msg); onDataLoaded?.([]); } finally { setIsLoading(false); } } fetchPosts(); }, [limit]); if (isLoading) { return (
Loading casts from Farcaster...
); } if (errorMsg) { return (
{errorMsg}
); } if (posts.length === 0) { return null; // Return nothing if no posts (ephemeral behavior) } return (
{posts.map((post) => ( ))}
); } ================================================ FILE: src/components/OnchainAnalytics/OnchainAnalytics.css ================================================ .onchain-analytics-container { display: flex; flex-direction: column; gap: 1rem; } .onchain-analytics-container > *:not(.onchain-analytics-session-hashes) { max-width: 600px; } .onchain-analytics-loading { text-align: center; color: #888; font-size: 1.1rem; } .onchain-analytics-error { color: #c00; background: #fee; padding: 1rem; border-radius: 0.5rem; margin-bottom: 1rem; text-align: center; } .onchain-analytics-break { word-break: break-all; } .onchain-analytics-session-hashes { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; } .onchain-analytics-session-hash { font-family: monospace; font-size: 0.75rem; transition: background 0.2s; } .onchain-analytics-session-hash:hover { background: #e0e0e0; } ================================================ FILE: src/components/OnchainAnalytics/index.tsx ================================================ import { useEffect, useState } from "react"; import { baseSepoliaClient } from "@lib/viemProvider"; import "./OnchainAnalytics.css"; // ABI fragments for the analytics contract const analyticsABI = [ { inputs: [], name: "getAllSessionIds", outputs: [{ internalType: "bytes32[]", name: "", type: "bytes32[]" }], stateMutability: "view", type: "function", }, { inputs: [], name: "getSessionCount", outputs: [{ internalType: "uint256", name: "", type: "uint256" }], stateMutability: "view", type: "function", }, { inputs: [], name: "getAllPageViews", outputs: [ { internalType: "string[]", name: "", type: "string[]" }, { internalType: "uint256[]", name: "", type: "uint256[]" }, { internalType: "uint64[]", name: "", type: "uint64[]" }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "getAllEvents", outputs: [ { internalType: "string[]", name: "", type: "string[]" }, { internalType: "uint256[]", name: "", type: "uint256[]" }, { internalType: "uint64[]", name: "", type: "uint64[]" }, ], stateMutability: "view", type: "function", }, ] as const; interface PageView { page: string; views: number; } interface Event { event: string; count: number; } interface OnchainAnalyticsProps { contractAddress: `0x${string}`; } export default function OnchainAnalytics({ contractAddress }: OnchainAnalyticsProps) { const [pageViews, setPageViews] = useState([]); const [events, setEvents] = useState([]); const [sessionCount, setSessionCount] = useState(0n); const [sessionIds, setSessionIds] = useState<`0x${string}`[]>([]); const [isLoading, setIsLoading] = useState(true); const [errorMsg, setErrorMsg] = useState(null); useEffect(() => { async function fetchAnalytics() { setIsLoading(true); setErrorMsg(null); try { const [count, views, eventsData, ids] = await Promise.all([ baseSepoliaClient.readContract({ address: contractAddress, abi: analyticsABI, functionName: "getSessionCount", }), baseSepoliaClient.readContract({ address: contractAddress, abi: analyticsABI, functionName: "getAllPageViews", }), baseSepoliaClient.readContract({ address: contractAddress, abi: analyticsABI, functionName: "getAllEvents", }), baseSepoliaClient.readContract({ address: contractAddress, abi: analyticsABI, functionName: "getAllSessionIds", }), ]); setSessionCount(count as bigint); // Process page views if (Array.isArray(views) && views.length === 3) { const [pagePathsRaw, pageOccurrencesRaw] = views as readonly [readonly string[], readonly bigint[], unknown]; const pagePaths = [...pagePathsRaw]; const pageOccurrences = [...(pageOccurrencesRaw as readonly bigint[])].map(Number); if (Array.isArray(pagePaths) && Array.isArray(pageOccurrences)) { setPageViews( pagePaths .map((page, i) => ({ page, views: pageOccurrences[i], })) .filter(({ page }) => page !== "/test" && page !== "/posts/1546329600000-markdown-test") .sort((a, b) => b.views - a.views) .slice(0, 5) ); } } // Process events if (Array.isArray(eventsData) && eventsData.length === 3) { const [eventNamesRaw, eventOccurrencesRaw] = eventsData as readonly [ readonly string[], readonly bigint[], unknown, ]; const eventNames = [...eventNamesRaw]; const eventOccurrences = [...(eventOccurrencesRaw as readonly bigint[])].map(Number); if (Array.isArray(eventNames) && Array.isArray(eventOccurrences)) { setEvents( eventNames .map((event, i) => ({ event, count: eventOccurrences[i], })) .sort((a, b) => b.count - a.count) .slice(0, 3) ); } } setSessionIds(ids as `0x${string}`[]); } catch (error) { setErrorMsg("Error fetching analytics data: " + (error instanceof Error ? error.message : String(error))); console.error(errorMsg); } finally { setIsLoading(false); } } fetchAnalytics(); }, [contractAddress]); if (isLoading) { return (
Loading analytics data...
); } if (errorMsg) { return (
{errorMsg}
); } return (

{sessionCount.toString()} unique sessions have been recorded.

{pageViews.map((view, index) => ( ))}
Top 5 pages Views
{view.page} {view.views}
{events.map((event, index) => ( ))}
Top events Count
{event.event} {event.count}

Session Hashes

The keccack256 hashes generated are technically valid private keys. These private keys are effectively burned, since they are public. As such, they should never be used for anything.

Click on a hash to view its corresponding portrait:

{sessionIds.map((sessionId, index) => ( {sessionId} ))}
); } ================================================ FILE: src/components/RecentGlass/RecentGlass.css ================================================ .recent-glass-container { margin-top: auto; min-height: calc(50vh - 2rem); border-top: 1px solid var(--color-border); padding: 2rem; @media (max-width: 768px) { padding: 1rem; } display: flex; flex-direction: column; gap: 1rem; } .recent-glass-container h2 { margin: 0 0 1rem 0; font-size: 1.5rem; } .glass-photo { position: relative; display: block; overflow: hidden; border: 1px solid var(--color-border); text-decoration: none; } .glass-photo img { width: 100%; height: auto; display: block; } .overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.75); display: flex; align-items: center; justify-content: center; padding: 1rem; opacity: 0; transition: opacity 0.3s ease; } .glass-photo:hover .overlay { opacity: 1; } .exif-info { color: white; text-align: center; font-size: 0.85rem; line-height: 1.5; } .exif-line { margin-bottom: 0.25rem; } .caption-text { margin-top: 0.75rem; font-style: italic; opacity: 0.9; } .recent-glass-loading, .recent-glass-error, .recent-glass-empty { text-align: center; padding: 2rem; @media (max-width: 768px) { padding: 1rem; } color: var(--color-muted); } .recent-glass-error { color: var(--color-error, #e74c3c); } .view-all { text-align: center; margin-top: 1rem; } .view-all a { text-decoration: none; color: inherit; font-weight: 500; } .view-all a:hover { text-decoration: underline; } ================================================ FILE: src/components/RecentGlass/index.tsx ================================================ import { useEffect, useState } from "react"; import { Masonry } from "react-plock"; import "./RecentGlass.css"; interface GlassExif { camera?: string; lens?: string; aperture?: string; focal_length?: string; iso?: string; exposure_time?: string; } interface GlassPhoto { id: string; width: number; height: number; image640x640: string; share_url: string; created_at: string; caption?: string; exif?: GlassExif; } export default function RecentGlass() { const [photos, setPhotos] = useState([]); const [isLoading, setIsLoading] = useState(true); const [errorMsg, setErrorMsg] = useState(null); useEffect(() => { async function fetchGlassPhotos() { setIsLoading(true); setErrorMsg(null); try { const response = await fetch("/api/glass.json?limit=6"); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data: GlassPhoto[] = await response.json(); setPhotos(data); } catch (error) { const msg = "Error fetching Glass photos: " + (error instanceof Error ? error.message : String(error)); setErrorMsg(msg); console.error(msg); } finally { setIsLoading(false); } } fetchGlassPhotos(); }, []); if (isLoading) { return (

Recent from Glass

Loading photos from Glass...
); } return ( ); } ================================================ FILE: src/components/SocialFeeds/SocialFeeds.css ================================================ .social-feeds { display: grid; gap: 2rem; width: 100%; } .social-feeds-three { grid-template-columns: 1fr 1fr 1fr; } .social-feeds-two { grid-template-columns: 1fr 1fr; } .social-feeds-one { grid-template-columns: 1fr; max-width: 64ch; } @media (max-width: 1024px) { .social-feeds-three { grid-template-columns: 1fr 1fr; } } @media (max-width: 768px) { .social-feeds-three, .social-feeds-two { grid-template-columns: 1fr; } } .social-column { display: flex; flex-direction: column; gap: 1rem; } .social-column summary { cursor: pointer; font-weight: 600; padding: 0.5rem 0; list-style: none; } .social-column summary::-webkit-details-marker { display: none; } .social-column summary::marker { display: none; } .social-feed { display: flex; flex-direction: column; gap: 1rem; } .social-post { border: 1px solid var(--color-border); padding: 1rem; display: flex; flex-direction: column; gap: 0.75rem; } .social-post-link { text-decoration: none; color: inherit; display: flex; flex-direction: column; gap: 0.5rem; } .social-time { color: var(--color-muted); font-size: 0.875rem; } .social-text { margin: 0; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; } /* Image grid layouts */ .social-images { display: grid; gap: 0.25rem; border-radius: 0.5rem; overflow: hidden; } .social-images-1 { grid-template-columns: 1fr; } .social-images-2 { grid-template-columns: 1fr 1fr; } .social-images-3 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; } .social-images-3 .social-image:first-child { grid-row: span 2; } .social-images-4 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; } .social-image { display: block; overflow: hidden; } .social-image img { width: 100%; height: 100%; object-fit: cover; transition: opacity 0.2s ease; } .social-image:hover img { opacity: 0.9; } /* Video embed */ .social-video { border-radius: 0.5rem; overflow: hidden; } .social-video video { width: 100%; display: block; } /* External link embed */ .social-external { display: flex; flex-direction: column; border: 1px solid var(--color-border); border-radius: 0.5rem; overflow: hidden; text-decoration: none; color: inherit; transition: border-color 0.2s ease; } .social-external:hover { border-color: var(--color-muted); } .social-external-thumb { width: 100%; height: auto; max-height: 150px; object-fit: cover; } .social-external-content { padding: 0.75rem; display: flex; flex-direction: column; gap: 0.25rem; } .social-external-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .social-external-uri { font-size: 0.75rem; color: var(--color-muted); } /* Link embed */ .social-link { display: inline-block; padding: 0.5rem 0.75rem; border: 1px solid var(--color-border); border-radius: 0.25rem; text-decoration: none; color: var(--color-muted); font-size: 0.875rem; transition: border-color 0.2s ease; } .social-link:hover { border-color: var(--color-foreground); } /* Quote embed */ .social-quote { display: flex; padding: 0.75rem; border: 1px solid var(--color-border); border-radius: 0.5rem; text-decoration: none; color: var(--color-muted); font-size: 0.875rem; transition: border-color 0.2s ease; } .social-quote:hover { border-color: var(--color-foreground); } /* Glass exif */ .social-exif { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.75rem; color: var(--color-muted); } .social-post-glass .social-image { border-radius: 0.5rem; } .social-post-glass .social-image img { height: auto; object-fit: contain; } /* Loading state */ .social-loading { text-align: center; padding: 2rem; color: var(--color-muted); } /* View all link */ .social-view-all { text-align: center; margin-top: 0.5rem; } .social-view-all a { text-decoration: none; color: inherit; font-weight: 500; } .social-view-all a:hover { text-decoration: underline; } /* Responsive */ @media (max-width: 768px) { .social-post { padding: 0.75rem; } .social-images-3 .social-image:first-child { grid-row: span 1; } .social-images-3 { grid-template-columns: 1fr; grid-template-rows: auto; } } ================================================ FILE: src/components/SocialFeeds/index.tsx ================================================ import { useEffect, useState } from "react"; import "./SocialFeeds.css"; // Shared types interface BasePost { createdAt: string; postUrl: string; } interface TextPost extends BasePost { text: string; } // Bluesky types interface BlueskyImage { type: "image"; thumb: string; fullsize: string; alt: string; mimeType: string; } interface BlueskyVideo { type: "video"; url: string; mimeType: string; } interface BlueskyExternal { type: "external"; uri: string; title: string; description: string; thumb?: string; } interface BlueskyQuote { type: "quote"; uri: string; cid: string; } type BlueskyEmbed = | BlueskyImage | BlueskyVideo | BlueskyExternal | BlueskyQuote; interface BlueskyPost extends TextPost { uri: string; cid: string; embeds: BlueskyEmbed[]; } // Farcaster types interface FarcasterEmbed { type: "url" | "cast"; url?: string; castFid?: number; castHash?: string; } interface FarcasterPost extends TextPost { hash: string; embeds: FarcasterEmbed[]; } // Glass types interface GlassExif { camera?: string; lens?: string; aperture?: string; focal_length?: string; iso?: string; exposure_time?: string; } interface GlassPost extends BasePost { id: string; width: number; height: number; image640x640: string; share_url: string; caption: string; exif?: GlassExif; } // 5 days in milliseconds const STALE_THRESHOLD_MS = 5 * 24 * 60 * 60 * 1000; function isPostStale(post: BasePost): boolean { const postTime = new Date(post.createdAt).getTime(); return Date.now() - postTime > STALE_THRESHOLD_MS; } function filterFreshPosts(posts: T[]): T[] { return posts.filter((post) => !isPostStale(post)); } function formatDate(dateString: string): string { const date = new Date(dateString); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return "just now"; if (diffMins < 60) return `${diffMins}m`; if (diffHours < 24) return `${diffHours}h`; if (diffDays < 7) return `${diffDays}d`; return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined, }); } function isImageUrl(url: string): boolean { // Check for common image extensions if (/\.(jpg|jpeg|png|gif|webp)$/i.test(url)) return true; // Check for known image CDNs if (url.includes("imagedelivery.net")) return true; if (url.includes("i.imgur.com")) return true; if (url.includes("pbs.twimg.com")) return true; return false; } // Bluesky post card function BlueskyPostCard({ post }: { post: BlueskyPost }) { const images = post.embeds.filter( (e): e is BlueskyImage => e.type === "image", ); const otherEmbeds = post.embeds.filter((e) => e.type !== "image"); return ( ); } // Farcaster post card function FarcasterPostCard({ post }: { post: FarcasterPost }) { const imageEmbeds = post.embeds.filter( (e) => e.type === "url" && e.url && isImageUrl(e.url), ); const otherEmbeds = post.embeds.filter( (e) => !(e.type === "url" && e.url && isImageUrl(e.url)), ); return ( ); } // Glass post card function GlassPostCard({ post }: { post: GlassPost }) { return (
{post.caption {post.caption &&

{post.caption}

} {post.exif && (
{post.exif.camera && {post.exif.camera}} {post.exif.lens && {post.exif.lens}} {(post.exif.aperture || post.exif.focal_length || post.exif.iso || post.exif.exposure_time) && ( {[ post.exif.aperture, post.exif.focal_length, post.exif.iso, post.exif.exposure_time, ] .filter(Boolean) .join(" · ")} )}
)}
); } interface SocialFeedsProps { limit?: number; } export default function SocialFeeds({ limit = 10 }: SocialFeedsProps) { const [blueskyPosts, setBlueskyPosts] = useState([]); const [farcasterPosts, setFarcasterPosts] = useState([]); const [glassPosts, setGlassPosts] = useState([]); const [blueskyLoading, setBlueskyLoading] = useState(true); const [farcasterLoading, setFarcasterLoading] = useState(true); const [glassLoading, setGlassLoading] = useState(true); useEffect(() => { // Fetch Bluesky fetch(`/api/bluesky.json?limit=${limit}&_=${Date.now()}`) .then((res) => res.json()) .then((data) => setBlueskyPosts(data)) .catch((err) => console.error("Bluesky error:", err)) .finally(() => setBlueskyLoading(false)); // Fetch Farcaster fetch(`/api/farcaster.json?limit=${limit}&_=${Date.now()}`) .then((res) => res.json()) .then((data) => setFarcasterPosts(data)) .catch((err) => console.error("Farcaster error:", err)) .finally(() => setFarcasterLoading(false)); // Fetch Glass fetch(`/api/glass.json?limit=${limit}&_=${Date.now()}`) .then((res) => res.json()) .then((data: any[]) => { // Transform Glass API response to match our types const posts: GlassPost[] = data.map((photo) => ({ id: photo.id, width: photo.width, height: photo.height, image640x640: photo.image640x640, share_url: photo.share_url, createdAt: photo.created_at, postUrl: photo.share_url, caption: photo.caption || "", exif: photo.exif, })); setGlassPosts(posts); }) .catch((err) => console.error("Glass error:", err)) .finally(() => setGlassLoading(false)); }, [limit]); const isLoading = blueskyLoading || farcasterLoading || glassLoading; // Filter to only fresh posts (within last 5 days) const freshBlueskyPosts = filterFreshPosts(blueskyPosts); const freshFarcasterPosts = filterFreshPosts(farcasterPosts); const freshGlassPosts = filterFreshPosts(glassPosts); const blueskyActive = !blueskyLoading && freshBlueskyPosts.length > 0; const farcasterActive = !farcasterLoading && freshFarcasterPosts.length > 0; const glassActive = !glassLoading && freshGlassPosts.length > 0; // Build feeds array with most recent post time for sorting type FeedConfig = { name: string; active: boolean; mostRecent: number; render: () => JSX.Element; }; const feeds: FeedConfig[] = [ { name: "bluesky", active: blueskyActive, mostRecent: freshBlueskyPosts[0] ? new Date(freshBlueskyPosts[0].createdAt).getTime() : 0, render: () => (
Bluesky
{freshBlueskyPosts.map((post) => ( ))}
), }, { name: "farcaster", active: farcasterActive, mostRecent: freshFarcasterPosts[0] ? new Date(freshFarcasterPosts[0].createdAt).getTime() : 0, render: () => (
Farcaster
{freshFarcasterPosts.map((post) => ( ))}
), }, { name: "glass", active: glassActive, mostRecent: freshGlassPosts[0] ? new Date(freshGlassPosts[0].createdAt).getTime() : 0, render: () => (
Glass
{freshGlassPosts.map((post) => ( ))}
), }, ]; // Filter active feeds and sort by most recent post const activeFeeds = feeds .filter((feed) => feed.active) .sort((a, b) => b.mostRecent - a.mostRecent); if (isLoading) { return
Loading social feeds...
; } if (activeFeeds.length === 0) { return null; // No active feeds } const gridClass = activeFeeds.length === 3 ? "social-feeds-three" : activeFeeds.length === 2 ? "social-feeds-two" : "social-feeds-one"; return (
{activeFeeds.map((feed) => feed.render())}
); } ================================================ FILE: src/components/footer.astro ================================================ --- --- ================================================ FILE: src/components/homepage/heroSection.astro ================================================ --- ---

Hi, I am Matthias

I run DevRel at Pinata and have a small growth consustancy called day---break on the side.

Over the years I've worked as a photographer, and have built growth teams in travel, fintech, and ecommerce.

This site is a collection notes, recipews, and thoughts. You'll find projects, ongoing and final, and recomendations for all sorts of things.

You can find me on various social platforms, subscribe to my rss feed, or find me on the small-web via Gemini: gemini://gem.iammatthias.com

================================================ FILE: src/components/homepage/recentContent.astro ================================================ --- import { getCollection } from "astro:content"; import { getGitHubCollections, type GitHubContentData } from "@lib/github-loader"; type Entry = { id: string; data: GitHubContentData; }; // Get all collection names const GITHUB_OWNER = "iammatthias"; const GITHUB_REPO = "obsidian_cms"; const GITHUB_TOKEN = import.meta.env.GITHUB; const collectionNames = await getGitHubCollections(GITHUB_OWNER, GITHUB_REPO, GITHUB_TOKEN, "main", "content"); // Get recent entries from each collection. The loader sorts at insertion // time, but Astro's content store doesn't guarantee insertion order on // read, so re-sort here. const recentByCollection = await Promise.all( collectionNames.map(async (name) => { try { const entries = ((await getCollection(name as any)) as Entry[]).sort( (a, b) => new Date(b.data.created || 0).getTime() - new Date(a.data.created || 0).getTime(), ); return { name, recent: entries.slice(0, 3), }; } catch (error) { console.error(`Error loading collection ${name}:`, error); return { name, recent: [] as Entry[], }; } }) ); // Filter out collections with no recent content const collectionsWithContent = recentByCollection .filter((c) => c.recent.length > 0) .sort((a, b) => { const dateA = new Date(a.recent[0].data.created).getTime(); const dateB = new Date(b.recent[0].data.created).getTime(); return dateB - dateA; // Most recent first }); ---
{ collectionsWithContent.map((collection) => (

{collection.name}

)) }
{ collectionsWithContent.length > 0 && ( ) }
================================================ FILE: src/components/homepage/recentFeeds.astro ================================================ --- ---

feeds

blogs I like that have rss

  • item 1
  • item 2
  • item 3
================================================ FILE: src/components/nav.astro ================================================ --- --- ================================================ FILE: src/components/sidebar/index.astro ================================================ --- --- ================================================ FILE: src/content.config.ts ================================================ import { defineCollection } from 'astro:content'; import { githubLoader, getGitHubCollections } from './lib/github-loader'; import { tagsLoader } from './lib/tags-loader'; // GitHub configuration const GITHUB_OWNER = 'iammatthias'; const GITHUB_REPO = 'obsidian_cms'; const GITHUB_BRANCH = 'main'; const GITHUB_TOKEN = import.meta.env.GITHUB; // Dynamically fetch collections from GitHub const collectionNames = await getGitHubCollections( GITHUB_OWNER, GITHUB_REPO, GITHUB_TOKEN, GITHUB_BRANCH, 'content' ); // Create a collection definition for each folder const collections = Object.fromEntries( collectionNames.map((collectionName) => [ collectionName, defineCollection({ loader: githubLoader({ owner: GITHUB_OWNER, repo: GITHUB_REPO, branch: GITHUB_BRANCH, contentPath: 'content', token: GITHUB_TOKEN, }), }), ]) ); // Add tags collection collections.tags = defineCollection({ loader: tagsLoader({ owner: GITHUB_OWNER, repo: GITHUB_REPO, token: GITHUB_TOKEN, branch: GITHUB_BRANCH, contentPath: 'content', }), }); export { collections }; ================================================ FILE: src/layouts/defaultLayouts.astro ================================================ --- import "@src/styles/reset.css"; import "@src/styles/globals.css"; import Nav from "@components/nav.astro"; import Footer from "@components/footer.astro"; interface Props { title?: string; description?: string; ogImage?: string; } const { title = "@iammatthias", description = "Personal site of Matthias Jordan", ogImage = "/" } = Astro.props; const canonicalURL = new URL(Astro.url.pathname, Astro.site); // Use OG subdomain for production, localhost for development const ogImageURL = import.meta.env.DEV ? new URL(ogImage, "http://localhost:8787") : new URL(ogImage, "https://og.iammatthias.com"); --- {title}
================================================ FILE: src/lib/github-loader.ts ================================================ import type { Loader, LoaderContext } from "astro/loaders"; import { z } from "astro/zod"; interface GitHubLoaderOptions { owner: string; repo: string; branch?: string; contentPath?: string; token: string; } export const githubContentSchema = z.object({ title: z.string(), slug: z.string(), published: z.boolean(), created: z.string(), updated: z.string(), tags: z.array(z.string()).optional(), excerpt: z.string().optional(), }); export type GitHubContentData = z.infer; // ============================================================================ // CACHE SYSTEM - Single fetch, shared across all loaders // ============================================================================ interface CachedFile { name: string; content: string; frontmatter: Record; body: string; } interface CachedCollection { name: string; files: CachedFile[]; } interface ContentCache { collections: CachedCollection[]; collectionNames: string[]; tagMap: Map>; fetched: boolean; } // Global cache - populated once, used by all loaders const contentCache: ContentCache = { collections: [], collectionNames: [], tagMap: new Map(), fetched: false, }; /** * Fetch all content from GitHub in a single API call and populate the cache. * This is called once and the results are shared across all loaders. */ async function populateCache( owner: string, repo: string, token: string, branch: string, contentPath: string, ): Promise { if (contentCache.fetched) { return; } const isDev = import.meta.env.DEV; // Single GraphQL query to get ALL content const query = ` query($owner: String!, $repo: String!, $expression: String!) { repository(owner: $owner, name: $repo) { object(expression: $expression) { ... on Tree { entries { name type object { ... on Tree { entries { name type object { ... on Blob { text } } } } } } } } } } `; const response = await fetch("https://api.github.com/graphql", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ query, variables: { owner, repo, expression: `${branch}:${contentPath}`, }, }), }); const result = await response.json(); if (result.errors) { throw new Error(`Failed to fetch content: ${result.errors[0].message}`); } const entries = result.data?.repository?.object?.entries || []; // Process each collection directory for (const entry of entries) { if (entry.type !== "tree") continue; const collectionName = entry.name; const markdownFiles = entry.object?.entries?.filter( (file: any) => file.type === "blob" && file.name.endsWith(".md"), ) || []; const cachedFiles: CachedFile[] = []; let hasPublishedContent = false; for (const file of markdownFiles) { const content = file.object?.text || ""; const { frontmatter, body } = parseFrontmatter(content); // Check if published (or in dev mode) const isPublished = isDev || frontmatter.published === true; if (isPublished) { hasPublishedContent = true; } cachedFiles.push({ name: file.name, content, frontmatter, body, }); // Build tag map while we're at it if (isPublished && frontmatter.tags && Array.isArray(frontmatter.tags)) { for (const tag of frontmatter.tags) { if (!contentCache.tagMap.has(tag)) { contentCache.tagMap.set(tag, new Set()); } contentCache.tagMap.get(tag)!.add(collectionName); } } } // Only include collections with published content if (hasPublishedContent) { contentCache.collectionNames.push(collectionName); contentCache.collections.push({ name: collectionName, files: cachedFiles, }); } } contentCache.fetched = true; } /** * Get cached collection data */ export function getCachedCollection( collectionName: string, ): CachedCollection | undefined { return contentCache.collections.find((c) => c.name === collectionName); } /** * Get cached tag map */ export function getCachedTagMap(): Map> { return contentCache.tagMap; } /** * Ensure cache is populated */ export async function ensureCachePopulated( owner: string, repo: string, token: string, branch = "main", contentPath = "content", ): Promise { await populateCache(owner, repo, token, branch, contentPath); } // ============================================================================ // GITHUB LOADER // ============================================================================ export function githubLoader(options: GitHubLoaderOptions): Loader { const { owner, repo, branch = "main", contentPath = "content", token, } = options; return { name: "github-markdown-loader", load: async ({ collection, store, logger, parseData, generateDigest, renderMarkdown, }: LoaderContext) => { logger.info(`Loading collection '${collection}' from GitHub`); const isDev = import.meta.env.DEV; try { // Ensure cache is populated await ensureCachePopulated(owner, repo, token, branch, contentPath); // Get cached collection data const cachedCollection = getCachedCollection(collection); if (!cachedCollection) { logger.warn(`Collection '${collection}' not found in cache`); return; } // Collect all entries for sorting const allEntries: Array<{ id: string; data: GitHubContentData; body: string; rendered: any; digest: string; }> = []; // Process each cached file for (const file of cachedCollection.files) { const { frontmatter, body } = file; // Skip unpublished entries in production if (!isDev && frontmatter.published !== true) { logger.info(`Skipping unpublished entry: ${file.name}`); continue; } // Use slug from frontmatter as ID, or fall back to filename const id = frontmatter.slug || file.name.replace(/\.md$/, ""); // Transform IPFS URIs to CDN URLs before rendering const processedBody = body.replace( /ipfs:\/\//g, "https://cdn.iammatthias.com/ipfs/", ); // Render markdown to HTML const rendered = await renderMarkdown(processedBody); // Parse and validate data according to schema const data = (await parseData({ id, data: { ...frontmatter, }, })) as GitHubContentData; // Generate digest for caching const digest = generateDigest(data); allEntries.push({ id, data, body: processedBody, rendered, digest, }); } // Sort by created date (most recent first) allEntries.sort((a, b) => { const dateA = new Date(a.data.created || 0).getTime(); const dateB = new Date(b.data.created || 0).getTime(); return dateB - dateA; }); // Store sorted entries store.clear(); for (const entry of allEntries) { store.set({ id: entry.id, data: entry.data, body: entry.body, rendered: entry.rendered, digest: entry.digest, }); logger.info(`Loaded: ${entry.id}`); } logger.info( `Successfully loaded ${allEntries.length} entries for ${collection} (${isDev ? "dev" : "prod"} mode)`, ); } catch (error) { logger.error(`Error loading collection ${collection}: ${error}`); throw error; } }, schema: githubContentSchema, }; } // ============================================================================ // HELPERS // ============================================================================ /** * Parse YAML frontmatter from markdown content */ function parseFrontmatter(content: string): { frontmatter: Record; body: string; } { const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/; const match = content.match(frontmatterRegex); if (!match) { return { frontmatter: {}, body: content }; } const [, frontmatterText, body] = match; const frontmatter = parseYaml(frontmatterText); return { frontmatter, body }; } /** * Simple YAML parser for frontmatter */ function parseYaml(yaml: string): Record { const result: Record = {}; const lines = yaml.split("\n"); let currentKey: string | null = null; let currentArray: any[] = []; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; // Array item if (trimmed.startsWith("- ")) { const value = trimmed.slice(2).trim(); currentArray.push(value); continue; } // Key-value pair const colonIndex = trimmed.indexOf(":"); if (colonIndex !== -1) { // Save previous array if exists if (currentKey && currentArray.length > 0) { result[currentKey] = currentArray; currentArray = []; } const key = trimmed.slice(0, colonIndex).trim(); let value = trimmed.slice(colonIndex + 1).trim(); // Remove quotes if present if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } currentKey = key; // Check if it's a boolean if (value === "true") { result[key] = true; } else if (value === "false") { result[key] = false; } else if (value === "") { // Empty value means an array might follow currentArray = []; } else { result[key] = value; } } } // Save final array if exists if (currentKey && currentArray.length > 0) { result[currentKey] = currentArray; } return result; } /** * Get all available collections from the GitHub repo that have published content. * Uses cached data - only fetches from GitHub once. */ export async function getGitHubCollections( owner: string, repo: string, token: string, branch = "main", contentPath = "content", ): Promise { await ensureCachePopulated(owner, repo, token, branch, contentPath); return contentCache.collectionNames; } ================================================ FILE: src/lib/og-renderer.ts ================================================ import satori from "satori"; import { html } from "satori-html"; import { Resvg } from "@cf-wasm/resvg"; /** * Renders HTML to PNG using @cf-wasm/resvg (works in both Node.js and Cloudflare Workers) */ export async function renderToPng( htmlContent: string, options: { width: number; height: number; fonts: Array<{ name: string; data: ArrayBuffer | Uint8Array; style: string; weight: number; }>; } ): Promise { // Generate SVG with satori const svg = await satori(html(htmlContent), options); // Convert SVG to PNG using @cf-wasm/resvg const resvg = new Resvg(svg); const pngData = resvg.render(); return pngData.asPng(); } ================================================ FILE: src/lib/tags-loader.ts ================================================ import type { Loader, LoaderContext } from "astro/loaders"; import { z } from "astro/zod"; import { ensureCachePopulated, getCachedTagMap } from "./github-loader"; interface TagsLoaderOptions { owner: string; repo: string; token: string; branch?: string; contentPath?: string; } export const tagsSchema = z.object({ name: z.string(), count: z.number(), collections: z.array(z.string()), }); export type TagData = z.infer; export function tagsLoader(options: TagsLoaderOptions): Loader { const { owner, repo, token, branch = "main", contentPath = "content", } = options; return { name: "tags-loader", load: async ({ store, logger, parseData, generateDigest, }: LoaderContext) => { logger.info("Loading tags from all collections"); try { // Ensure cache is populated (this is a no-op if already done) await ensureCachePopulated(owner, repo, token, branch, contentPath); // Get the pre-built tag map from the cache const tagMap = getCachedTagMap(); // Convert tag map to entries store.clear(); for (const [tagName, collections] of tagMap.entries()) { const data = (await parseData({ id: tagName, data: { name: tagName, count: collections.size, collections: Array.from(collections), }, })) as TagData; const digest = generateDigest(data); store.set({ id: tagName, data, digest, }); logger.info( `Loaded tag: ${tagName} (${collections.size} collections)`, ); } logger.info(`Successfully loaded ${tagMap.size} tags`); } catch (error) { logger.error(`Error loading tags: ${error}`); throw error; } }, schema: tagsSchema, }; } ================================================ FILE: src/lib/viemProvider.ts ================================================ import { createPublicClient, http } from 'viem'; import { baseSepolia } from 'viem/chains'; export const baseSepoliaClient = createPublicClient({ chain: baseSepolia, transport: http(), }); ================================================ FILE: src/pages/404.astro ================================================ --- export const prerender = false; import DefaultLayout from "@layouts/defaultLayouts.astro"; import BlackHole from "@src/components/BlackHole"; import { keccak_256 } from "@noble/hashes/sha3.js"; import { bytesToHex } from "@noble/hashes/utils.js"; // Generate seed from session details const sessionData = [ Astro.request.headers.get("user-agent") || "", Astro.request.headers.get("accept-language") || "", Astro.clientAddress || "", Date.now().toString(), ].join("|"); const hash = keccak_256(new TextEncoder().encode(sessionData)); const seed = parseInt(bytesToHex(hash).slice(0, 8), 16); const title = "404 - Page Not Found"; const description = "The page you're looking for doesn't exist"; const ogImage = "/404"; ---

404

Page not found

Return home
================================================ FILE: src/pages/about.astro ================================================ --- import DefaultLayout from "@layouts/defaultLayouts.astro"; const title = "About - @iammatthias"; const description = "Hi, I am Matthias. I'm a photographer turned growth engineer working on the future of the web."; const ogImage = "/about"; ---

Hi, I am Matthias.

I'm a photographer turned developer relations engineer. I've gotten to do some really cool things along the way, and now I'm working on the future of the web.

This site is my cozy corner on the web. It is a collection of thoughts, projects, and ideas. You can keep up with my career by checking out my resume, or follow along a bit more ephemerally by checking out what I'm up to now.

Currently based in Southern California, where I spend a lot of time cooking or behind the lens when I'm not at my desk.

================================================ FILE: src/pages/api/bluesky.json.ts ================================================ import type { APIRoute } from "astro"; // PDS configuration const USER_DID = "did:plc:p5xem22ammiafn5kxonaksfa"; const PDS_HOST = "https://pds.iammatthias.com"; // Raw record types from PDS interface BlobRef { $type: "blob"; ref: { $link: string }; mimeType: string; size: number; } interface RawImageEmbed { $type: "app.bsky.embed.images"; images: Array<{ alt: string; image: BlobRef; aspectRatio?: { width: number; height: number }; }>; } interface RawVideoEmbed { $type: "app.bsky.embed.video"; video: BlobRef; alt?: string; aspectRatio?: { width: number; height: number }; } interface RawExternalEmbed { $type: "app.bsky.embed.external"; external: { uri: string; title: string; description: string; thumb?: BlobRef; }; } interface RawRecordEmbed { $type: "app.bsky.embed.record"; record: { uri: string; cid: string; }; } interface RawRecordWithMediaEmbed { $type: "app.bsky.embed.recordWithMedia"; record: RawRecordEmbed; media: RawImageEmbed | RawVideoEmbed | RawExternalEmbed; } type RawEmbed = | RawImageEmbed | RawVideoEmbed | RawExternalEmbed | RawRecordEmbed | RawRecordWithMediaEmbed; interface RawPostRecord { $type: "app.bsky.feed.post"; text: string; createdAt: string; embed?: RawEmbed; reply?: { root: { uri: string; cid: string }; parent: { uri: string; cid: string }; }; langs?: string[]; facets?: Array<{ index: { byteStart: number; byteEnd: number }; features: Array<{ $type: string; uri?: string; did?: string; tag?: string; }>; }>; } interface PDSRecord { uri: string; cid: string; value: RawPostRecord; } interface ListRecordsResponse { records: PDSRecord[]; cursor?: string; } // Output types - normalized for the component interface NormalizedImage { type: "image"; thumb: string; fullsize: string; alt: string; aspectRatio?: { width: number; height: number }; mimeType: string; } interface NormalizedVideo { type: "video"; url: string; alt?: string; aspectRatio?: { width: number; height: number }; mimeType: string; } interface NormalizedExternal { type: "external"; uri: string; title: string; description: string; thumb?: string; } interface NormalizedQuote { type: "quote"; uri: string; cid: string; } type NormalizedEmbed = | NormalizedImage | NormalizedVideo | NormalizedExternal | NormalizedQuote; // Build blob URL from PDS function getBlobUrl(did: string, cid: string): string { return `${PDS_HOST}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`; } function normalizeEmbed(embed: RawEmbed, did: string): NormalizedEmbed[] { const embeds: NormalizedEmbed[] = []; switch (embed.$type) { case "app.bsky.embed.images": for (const img of embed.images) { const blobCid = img.image.ref.$link; const url = getBlobUrl(did, blobCid); embeds.push({ type: "image", thumb: url, fullsize: url, alt: img.alt || "", aspectRatio: img.aspectRatio, mimeType: img.image.mimeType, }); } break; case "app.bsky.embed.video": const videoCid = embed.video.ref.$link; embeds.push({ type: "video", url: getBlobUrl(did, videoCid), alt: embed.alt, aspectRatio: embed.aspectRatio, mimeType: embed.video.mimeType, }); break; case "app.bsky.embed.external": embeds.push({ type: "external", uri: embed.external.uri, title: embed.external.title, description: embed.external.description, thumb: embed.external.thumb ? getBlobUrl(did, embed.external.thumb.ref.$link) : undefined, }); break; case "app.bsky.embed.record": embeds.push({ type: "quote", uri: embed.record.uri, cid: embed.record.cid, }); break; case "app.bsky.embed.recordWithMedia": // Process the media portion if (embed.media) { embeds.push(...normalizeEmbed(embed.media, did)); } // Process the quoted record if (embed.record) { embeds.push(...normalizeEmbed(embed.record, did)); } break; } return embeds; } export const prerender = false; export const GET: APIRoute = async ({ url }) => { try { // Get limit from query params, default to 10 const limitParam = url.searchParams.get("limit"); const limit = limitParam ? parseInt(limitParam) : 10; // Fetch records directly from PDS - no auth needed for public records const recordsUrl = `${PDS_HOST}/xrpc/com.atproto.repo.listRecords?repo=${USER_DID}&collection=app.bsky.feed.post&limit=${limit}&reverse=true`; const response = await fetch(recordsUrl, { method: "GET", headers: { Accept: "application/json", "User-Agent": "iammatthias.com/1.0", }, }); if (!response.ok) { throw new Error( `PDS API responded with ${response.status}: ${response.statusText}`, ); } const data: ListRecordsResponse = await response.json(); // Process and transform the posts, sort by date descending (newest first) const posts = data.records .filter((record) => { // Filter out replies, only show original posts return !record.value.reply; }) .sort( (a, b) => new Date(b.value.createdAt).getTime() - new Date(a.value.createdAt).getTime(), ) .map((record) => { const post = record.value; // Normalize all embeds const embeds: NormalizedEmbed[] = post.embed ? normalizeEmbed(post.embed, USER_DID) : []; // Extract post ID from URI for the link const uriParts = record.uri.split("/"); const postId = uriParts[uriParts.length - 1]; return { uri: record.uri, cid: record.cid, text: post.text, createdAt: post.createdAt, embeds, facets: post.facets, // No engagement counts from raw PDS records postUrl: `https://bsky.app/profile/${USER_DID}/post/${postId}`, }; }) .slice(0, limit); return new Response(JSON.stringify(posts), { status: 200, headers: { "Content-Type": "application/json", "Cache-Control": "no-cache, no-store, must-revalidate", }, }); } catch (error) { console.error("Error fetching from PDS:", error); // Return empty array on error to allow the site to function return new Response(JSON.stringify([]), { status: 200, headers: { "Content-Type": "application/json", "Cache-Control": "no-cache, no-store, must-revalidate", }, }); } }; ================================================ FILE: src/pages/api/farcaster.json.ts ================================================ import type { APIRoute } from "astro"; // Farcaster Hub configuration const HUB_URL = "https://hub.merv.fun"; const USER_FID = 2728; // Farcaster epoch starts Jan 1, 2021 00:00:00 UTC const FARCASTER_EPOCH = 1609459200; interface FarcasterEmbed { url?: string; castId?: { fid: number; hash: string; }; } interface FarcasterCastAddBody { text: string; embeds: FarcasterEmbed[]; mentions: number[]; mentionsPositions: number[]; parentCastId?: { fid: number; hash: string; }; parentUrl?: string; } interface FarcasterMessage { data: { type: string; fid: number; timestamp: number; castAddBody?: FarcasterCastAddBody; }; hash: string; } interface FarcasterResponse { messages: FarcasterMessage[]; nextPageToken?: string; } // Output types interface NormalizedEmbed { type: "url" | "cast"; url?: string; castFid?: number; castHash?: string; } function farcasterTimestampToDate(timestamp: number): Date { return new Date((FARCASTER_EPOCH + timestamp) * 1000); } export const prerender = false; export const GET: APIRoute = async ({ url }) => { try { const limitParam = url.searchParams.get("limit"); const limit = limitParam ? parseInt(limitParam) : 10; // Fetch more casts than needed since we filter out replies const fetchSize = Math.max(limit * 3, 30); // Fetch casts from Farcaster hub with timeout const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15000); const response = await fetch( `${HUB_URL}/v1/castsByFid?fid=${USER_FID}&pageSize=${fetchSize}&reverse=true`, { method: "GET", headers: { Accept: "application/json", }, signal: controller.signal, }, ); clearTimeout(timeout); if (!response.ok) { throw new Error( `Farcaster hub responded with ${response.status}: ${response.statusText}`, ); } const data: FarcasterResponse = await response.json(); // Process casts - filter to only top-level posts (no replies) const posts = data.messages .filter((msg) => { // Only include cast adds that are not replies return ( msg.data.type === "MESSAGE_TYPE_CAST_ADD" && msg.data.castAddBody && !msg.data.castAddBody.parentCastId && !msg.data.castAddBody.parentUrl ); }) .map((msg) => { const cast = msg.data.castAddBody!; const createdAt = farcasterTimestampToDate(msg.data.timestamp); // Normalize embeds const embeds: NormalizedEmbed[] = cast.embeds .map((embed) => { if (embed.url) { return { type: "url" as const, url: embed.url }; } if (embed.castId) { return { type: "cast" as const, castFid: embed.castId.fid, castHash: embed.castId.hash, }; } return { type: "url" as const }; }) .filter((e) => e.url || e.castHash); // Build warpcast URL const hashHex = msg.hash.startsWith("0x") ? msg.hash.slice(2) : msg.hash; const postUrl = `https://warpcast.com/~/conversations/${hashHex}`; return { hash: msg.hash, text: cast.text, createdAt: createdAt.toISOString(), embeds, postUrl, }; }) .slice(0, limit); return new Response(JSON.stringify(posts), { status: 200, headers: { "Content-Type": "application/json", "Cache-Control": "no-cache, no-store, must-revalidate", }, }); } catch (error) { console.error("Error fetching from Farcaster:", error); return new Response(JSON.stringify([]), { status: 200, headers: { "Content-Type": "application/json", "Cache-Control": "no-cache, no-store, must-revalidate", }, }); } }; ================================================ FILE: src/pages/api/glass.json.ts ================================================ import type { APIRoute } from "astro"; import pLimit from "p-limit"; // Glass API types - matches actual API response structure interface GlassExif { camera?: string; lens?: string; aperture?: string; focal_length?: string; iso?: string; exposure_time?: string; } interface GlassPhoto { id: string; width: number; height: number; image640x640: string; share_url: string; created_at: string; description?: string; exif?: GlassExif; } // Limit concurrent requests to Glass API const glassLimit = pLimit(2); export const prerender = false; export const GET: APIRoute = async ({ url }) => { try { // Get limit from query params, default to 50 const limitParam = url.searchParams.get("limit"); const limit = limitParam ? parseInt(limitParam) : 50; // Fetch directly from Glass API const glassResponse = await glassLimit(async () => { // Glass.photo API endpoint - this may need to be adjusted based on their actual API // Using the user profile API for @iam const glassApiUrl = `https://glass.photo/api/v3/users/iam/posts?limit=${limit}`; const response = await fetch(glassApiUrl, { method: "GET", headers: { "Content-Type": "application/json", "User-Agent": "iammatthias.com/1.0", // Identify our site }, }); if (!response.ok) { throw new Error(`Glass API responded with ${response.status}: ${response.statusText}`); } return response.json(); }); // Process and validate the photos - API returns array directly, not wrapped in an object const photos: GlassPhoto[] = Array.isArray(glassResponse) ? glassResponse : []; const validPhotos = photos .filter((photo): photo is GlassPhoto => { return ( photo && typeof photo.id === "string" && typeof photo.width === "number" && typeof photo.height === "number" && typeof photo.image640x640 === "string" && typeof photo.share_url === "string" && typeof photo.created_at === "string" ); }) .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) .slice(0, limit) .map((photo) => ({ id: photo.id, width: photo.width, height: photo.height, image640x640: photo.image640x640, share_url: photo.share_url, created_at: photo.created_at, caption: photo.description || "", exif: photo.exif ? { camera: photo.exif.camera, lens: photo.exif.lens, aperture: photo.exif.aperture, focal_length: photo.exif.focal_length, iso: photo.exif.iso, exposure_time: photo.exif.exposure_time, } : undefined, })); return new Response(JSON.stringify(validPhotos), { status: 200, headers: { "Content-Type": "application/json", "Cache-Control": "public, max-age=300", // Cache for 5 minutes }, }); } catch (error) { console.error("Error fetching from Glass API:", error); // For development/fallback, return empty array instead of error // This allows the site to function even if Glass API is unavailable return new Response(JSON.stringify([]), { status: 200, headers: { "Content-Type": "application/json", "Cache-Control": "public, max-age=60", // Shorter cache for errors }, }); } }; ================================================ FILE: src/pages/content/[collection]/[slug].astro ================================================ --- import { getCollection, render } from "astro:content"; import DefaultLayout from "@layouts/defaultLayouts.astro"; import { getGitHubCollections, type GitHubContentData } from "@lib/github-loader"; import Sidebar from "@components/sidebar/index.astro"; type Entry = { id: string; data: GitHubContentData; }; export async function getStaticPaths() { const GITHUB_OWNER = "iammatthias"; const GITHUB_REPO = "obsidian_cms"; const GITHUB_TOKEN = import.meta.env.GITHUB; const collectionNames = await getGitHubCollections(GITHUB_OWNER, GITHUB_REPO, GITHUB_TOKEN, "main", "content"); const paths = await Promise.all( collectionNames.map(async (collectionName) => { const entries = (await getCollection(collectionName as any)) as Entry[]; return entries.map((entry) => ({ params: { collection: collectionName, slug: entry.id, }, props: { entry }, })); }) ); return paths.flat(); } interface Props { entry: Entry; } const { entry } = Astro.props; const { collection, slug } = Astro.params; const { Content } = await render(entry as any); const title = entry.data.title || slug; const description = entry.data.excerpt || `${title} - ${collection}`; const ogImage = `/${collection}-${encodeURIComponent(title || "")}`; ---

{entry.data.title}

{ entry.data.tags && entry.data.tags.length > 0 && (
{entry.data.tags.map((tag: string) => ( {tag} ))}
) } {entry.data.excerpt &&

{entry.data.excerpt}

}
================================================ FILE: src/pages/content/[collection]/index.astro ================================================ --- import { getCollection } from "astro:content"; import DefaultLayout from "@layouts/defaultLayouts.astro"; import { getGitHubCollections, type GitHubContentData } from "@lib/github-loader"; export async function getStaticPaths() { const GITHUB_OWNER = "iammatthias"; const GITHUB_REPO = "obsidian_cms"; const GITHUB_TOKEN = import.meta.env.GITHUB; const collectionNames = await getGitHubCollections(GITHUB_OWNER, GITHUB_REPO, GITHUB_TOKEN, "main", "content"); return collectionNames.map((collection) => ({ params: { collection }, })); } type Entry = { id: string; data: GitHubContentData; }; const { collection } = Astro.params; // Get all entries in this collection. Re-sort by created desc — the loader // sorts at insertion time, but Astro's content store doesn't guarantee // insertion order on read. const entries = ((await getCollection(collection as any)) as Entry[]).sort( (a, b) => new Date(b.data.created || 0).getTime() - new Date(a.data.created || 0).getTime(), ); // Group by tags if available const byTag = new Map(); entries.forEach((entry) => { if (entry.data.tags) { entry.data.tags.forEach((tag: string) => { if (!byTag.has(tag)) { byTag.set(tag, []); } byTag.get(tag)!.push(entry); }); } }); const title = `${collection} - @iammatthias`; const description = `Browse ${entries.length} items in ${collection}`; const ogImage = `/${collection}`; ---

{collection}

{entries.length} items

RSS
{ entries.map((entry) => (

{entry.data.title}

{entry.data.excerpt &&

{entry.data.excerpt}

}
{entry.data.tags && entry.data.tags.length > 0 && (
{entry.data.tags.map((tag: string) => ( {tag} ))}
)}
)) }
{ byTag.size > 0 && ( ) }
================================================ FILE: src/pages/content/[collection]/rss.xml.ts ================================================ import rss from '@astrojs/rss'; import { getCollection } from 'astro:content'; import { getGitHubCollections, type GitHubContentData } from '@lib/github-loader'; import type { APIContext } from 'astro'; type Entry = { id: string; data: GitHubContentData; }; export async function getStaticPaths() { const GITHUB_OWNER = 'iammatthias'; const GITHUB_REPO = 'obsidian_cms'; const GITHUB_TOKEN = import.meta.env.GITHUB; const collectionNames = await getGitHubCollections( GITHUB_OWNER, GITHUB_REPO, GITHUB_TOKEN, 'main', 'content' ); return collectionNames.map((collection) => ({ params: { collection }, })); } export async function GET(context: APIContext) { const { collection } = context.params; if (!collection) { return new Response('Collection not found', { status: 404 }); } const entries = ((await getCollection(collection as any)) as Entry[]).sort( (a, b) => new Date(b.data.created || 0).getTime() - new Date(a.data.created || 0).getTime(), ); return rss({ title: `${collection} - iammatthias`, description: `Latest content from ${collection}`, site: context.site?.toString() || 'https://iammatthias.com', items: entries.map((entry) => ({ title: entry.data.title, pubDate: new Date(entry.data.created), description: entry.data.excerpt || '', link: `/content/${collection}/${entry.id}/`, categories: entry.data.tags || [], })), customData: 'en-us', stylesheet: '/rss.xml.xsl', }); } ================================================ FILE: src/pages/content/index.astro ================================================ --- import { getCollection } from "astro:content"; import DefaultLayout from "@layouts/defaultLayouts.astro"; import { getGitHubCollections, type GitHubContentData } from "@lib/github-loader"; type Entry = { id: string; data: GitHubContentData; }; // Get all collection names const GITHUB_OWNER = "iammatthias"; const GITHUB_REPO = "obsidian_cms"; const GITHUB_TOKEN = import.meta.env.GITHUB; const collectionNames = await getGitHubCollections(GITHUB_OWNER, GITHUB_REPO, GITHUB_TOKEN, "main", "content"); // Get all entries from all collections and flatten into one array // Note: Entries are already filtered by published status and sorted by recency at the loader level const allEntriesWithCollection = ( await Promise.all( collectionNames.map(async (name) => { const entries = (await getCollection(name as any)) as Entry[]; return entries.map((entry) => ({ ...entry, collection: name, })); }) ) ) .flat() .sort((a, b) => { const dateA = new Date(a.data.created).getTime(); const dateB = new Date(b.data.created).getTime(); return dateB - dateA; // Most recent first }); const title = "All Content - @iammatthias"; const description = `Browse all ${allEntriesWithCollection.length} items across collections`; const ogImage = "/content"; ---

All Content

{allEntriesWithCollection.length} items

RSS
{ allEntriesWithCollection.map((entry) => (

{entry.data.title}

{entry.data.excerpt &&

{entry.data.excerpt}

}
{entry.collection} {entry.data.tags && entry.data.tags.length > 0 && (
{entry.data.tags.map((tag: string) => ( {tag} ))}
)}
)) }
================================================ FILE: src/pages/index.astro ================================================ --- import DefaultLayout from "@layouts/defaultLayouts.astro"; import HeroSection from "@components/homepage/heroSection.astro"; import RecentContent from "@components/homepage/recentContent.astro"; import RecentGlass from "@components/RecentGlass"; // import RecentFeeds from "@components/homepage/recentFeeds.astro"; const title = "@iammatthias"; const description = "Personal site of Matthias Jordan - photographer turned growth engineer"; const ogImage = "/"; --- ================================================ FILE: src/pages/now.astro ================================================ --- import DefaultLayout from "@layouts/defaultLayouts.astro"; import SocialFeeds from "@components/SocialFeeds/index.tsx"; const title = "Now - @iammatthias"; const description = "Hi, I am Matthias. I'm a photographer turned growth engineer working on the future of the web."; const ogImage = "/now"; const socialLinks = [ { name: "Glass", url: "https://glass.photo/iam", username: "@iam", blurb: "Glass is a photography-centric platform, and is where I post most of my pictures.", }, { name: "Farcaster", url: "https://farcaster.xyz/iammatthias", username: "@iammatthias", blurb: 'Farcaster is a "sufficiently decentralized" social protocol. There are multiple clients, but I use Warpcast.', }, { name: "Bluesky", url: "https://bsky.app/profile/iammatthias.com", username: "@iammatthias.com", blurb: 'Not as active as it used to be', }, { name: "Instagram", url: "https://instagram.com/iammatthias", username: "@iammatthias", blurb: "Not as active as it used to be, I share pictures on Glass these days.", }, { name: "Threads", url: "https://www.threads.net/@iammatthias", username: "@iammatthias", blurb: "Mostly inactive.", }, { name: "GitHub", url: "https://github.com/iammatthias", username: "@iammatthias", blurb: "Work and side projects.", }, { name: "LinkedIn", url: "https://linkedin.com/in/iammatthias", username: "@iammatthias", blurb: "An up-to-date snapshot of my professional career.", }, { name: "Mastodon", url: "https://mastodon.social/@iammatthias", username: "@iammatthias", blurb: "I maintain an account, but I am not active.", }, { name: "Twitter", url: "https://twitter.com/iammatthias", username: "@iammatthias", blurb: "I maintain an account, but I have deleted almost all content.", }, ]; ---

Now

A short list of things I'm working on:

  • DevRel @ pinata
  • Building LLM TXT
  • Cooking - check out my collection of recipes

Last updated April 1st, 2026

Updates

Find me online

================================================ FILE: src/pages/onchain-analytics/[hash]/index.astro ================================================ --- export const prerender = false; import DefaultLayout from "@layouts/defaultLayouts.astro"; function hashToNumbers(hash: string): number[] { const cleanHash = hash.replace("0x", ""); return Array.from({ length: cleanHash.length / 2 }, (_, i) => parseInt(cleanHash.slice(i * 2, (i + 1) * 2), 16)); } function generatePatternData(hash: string) { const numbers = hashToNumbers(hash); // Generate background fill pattern function generateBackgroundPattern(): string[] { const patterns: string[] = []; const gridSizeX = 24; const gridSizeY = 28; // Adjusted for 6:7 ratio for (let x = 0; x < gridSizeX; x++) { for (let y = 0; y < gridSizeY; y++) { const baseX = (x / gridSizeX) * 120; const baseY = (y / gridSizeY) * 140; const offsetX = (numbers[(x + y) % numbers.length] / 255 - 0.5) * 4; const offsetY = (numbers[(x + y + 1) % numbers.length] / 255 - 0.5) * 4; const size = 0.15 + (numbers[(x * y) % numbers.length] / 255) * 0.2; patterns.push( `M ${baseX + offsetX},${baseY + offsetY} m ${-size},0 a ${size},${size} 0 1,0 ${size * 2},0 a ${size},${size} 0 1,0 ${-size * 2},0` ); } } return patterns; } function generatePrimaryRegions(): { centers: [number, number][]; paths: string[] } { const centers: [number, number][] = []; const numRegions = 12 + (numbers[0] % 4); // Increased for larger canvas // Create a grid of centers adjusted for 6:7 ratio const gridSizeX = 5; const gridSizeY = 6; for (let x = 0; x < gridSizeX; x++) { for (let y = 0; y < gridSizeY; y++) { const baseX = 20 + (x * 80) / (gridSizeX - 1); const baseY = 20 + (y * 100) / (gridSizeY - 1); const offsetX = (numbers[(x + y) % numbers.length] / 255 - 0.5) * 15; const offsetY = (numbers[(x + y + 1) % numbers.length] / 255 - 0.5) * 15; centers.push([baseX + offsetX, baseY + offsetY]); } } // Add random centers for organic feel for (let i = 0; i < numRegions - gridSizeX * gridSizeY; i++) { const angle = (numbers[i] / 255) * Math.PI * 2; const distance = 20 + (numbers[i + 1] / 255) * 45; centers.push([60 + Math.cos(angle) * distance, 70 + Math.sin(angle) * distance]); } const paths: string[] = centers.map((center, i) => { const points: [number, number][] = []; const numPoints = 24; for (let j = 0; j < numPoints; j++) { const angle = (j / numPoints) * Math.PI * 2; const noise = (numbers[(i + j) % numbers.length] / 255) * 20; const baseDistance = 35 + (numbers[(i * 2 + j) % numbers.length] / 255) * 25; const distance = baseDistance + noise; points.push([center[0] + Math.cos(angle) * distance, center[1] + Math.sin(angle) * distance]); } return ( `M ${points[0][0]},${points[0][1]} ` + points .slice(1) .map((p) => `L ${p[0]},${p[1]}`) .join(" ") + " Z" ); }); return { centers, paths }; } function generateCellPacking(regionCenter: [number, number], regionIndex: number): string[] { const cells: string[] = []; const regionRadius = 55; // Create dense spiral patterns const numSpirals = 4 + (numbers[regionIndex] % 3); for (let s = 0; s < numSpirals; s++) { const startAngle = (numbers[s * regionIndex] / 255) * Math.PI * 2; let angle = startAngle; let radius = 1; const maxDots = 60; let dotCount = 0; while (radius < regionRadius && dotCount < maxDots) { const angleNoise = (numbers[(Math.floor(angle * 5) + s) % numbers.length] / 255 - 0.5) * 0.4; const radiusStep = 1.2 + numbers[(s * dotCount) % numbers.length] / 255; angle += 0.3 + angleNoise; radius += radiusStep; const px = regionCenter[0] + Math.cos(angle) * radius; const py = regionCenter[1] + Math.sin(angle) * radius; const dotSize = 0.5 + (numbers[(dotCount * s) % numbers.length] / 255) * 0.7; cells.push( `M ${px},${py} m ${-dotSize},0 a ${dotSize},${dotSize} 0 1,0 ${dotSize * 2},0 a ${dotSize},${dotSize} 0 1,0 ${-dotSize * 2},0` ); dotCount++; } } // Add dense scattered fill const numScatterDots = 40 + (numbers[regionIndex] % 20); for (let i = 0; i < numScatterDots; i++) { const angle = (numbers[(regionIndex * i) % numbers.length] / 255) * Math.PI * 2; const radius = (numbers[(regionIndex * i + 1) % numbers.length] / 255) * regionRadius; const px = regionCenter[0] + Math.cos(angle) * radius; const py = regionCenter[1] + Math.sin(angle) * radius; const dotSize = 0.3 + (numbers[(i * 3) % numbers.length] / 255) * 0.5; cells.push( `M ${px},${py} m ${-dotSize},0 a ${dotSize},${dotSize} 0 1,0 ${dotSize * 2},0 a ${dotSize},${dotSize} 0 1,0 ${-dotSize * 2},0` ); } return cells; } function generateTuringPattern(regionCenter: [number, number], regionIndex: number): string[] { const lines: string[] = []; const regionRadius = 55; // Generate dense line coverage const numLines = 15 + (numbers[regionIndex] % 10); for (let i = 0; i < numLines; i++) { const points: [number, number][] = []; let x = regionCenter[0] + (numbers[i] / 255 - 0.5) * regionRadius; let y = regionCenter[1] + (numbers[i + 1] / 255 - 0.5) * regionRadius; const numSegments = 20 + (numbers[i * 2] % 15); for (let j = 0; j < numSegments; j++) { points.push([x, y]); const angleChange = (numbers[(i * j) % numbers.length] / 255 - 0.5) * Math.PI * 1.8; const stepSize = 2.5 + (numbers[(i * j + 1) % numbers.length] / 255) * 4; x += Math.cos(angleChange) * stepSize; y += Math.sin(angleChange) * stepSize; } if (points.length > 2) { const path = points.reduce((acc, point, idx) => { if (idx === 0) return `M ${point[0]} ${point[1]}`; if (idx % 2 === 0) { const prev = points[idx - 1]; const ctrlX = prev[0] + (numbers[(idx * regionIndex) % numbers.length] / 255 - 0.5) * 10; const ctrlY = prev[1] + (numbers[(idx * regionIndex + 1) % numbers.length] / 255 - 0.5) * 10; return `${acc} Q ${ctrlX} ${ctrlY}, ${point[0]} ${point[1]}`; } return acc; }, ""); lines.push(path); } } return lines; } const primaryRegions = generatePrimaryRegions(); const backgroundPattern = generateBackgroundPattern(); const patterns = primaryRegions.centers.map((center, i) => { // Adjust pattern type distribution for more balanced coverage const patternType = numbers[i] > 100; // Changed from 128 to 100 for more cell patterns return { type: patternType ? "cells" : "turing", elements: patternType ? generateCellPacking(center, i) : generateTuringPattern(center, i), }; }); return { background: backgroundPattern, regions: primaryRegions.paths, patterns, }; } function generateColors(hash: string) { const numbers = hashToNumbers(hash); const palette: string[] = []; // Generate fully hash-derived colors for (let i = 0; i < 4; i++) { const hue = (numbers[i * 3] / 255) * 360; const saturation = 60 + (numbers[i * 3 + 1] / 255) * 40; // Range: 60-100% const lightness = 20 + (numbers[i * 3 + 2] / 255) * 65; // Range: 20-85% palette.push(`hsl(${hue}, ${saturation}%, ${lightness}%)`); } // Sort colors by lightness to ensure background is darkest const sorted = [...palette].sort((a, b) => { const getLightness = (color: string) => { const match = color.match(/(\d+)%\)/); return match ? parseInt(match[1]) : 0; }; return getLightness(a) - getLightness(b); }); return sorted; } const { hash } = Astro.params; if (!hash) throw new Error("Hash parameter is required"); const patterns = generatePatternData(hash); const colors = generateColors(hash); const title = `Session ${hash.slice(0, 8)}... - @iammatthias`; const description = `Unique generative art pattern for session ${hash}`; const ogImage = `/og-session-${hash}.png`; ---
Back {patterns.background.map((path) => )} { patterns.patterns.map((pattern, i) => ( <> {pattern.type === "cells" ? pattern.elements.map((path) => ( )) : pattern.elements.map((path) => ( ))} )) }

{hash}

================================================ FILE: src/pages/onchain-analytics/index.astro ================================================ --- import DefaultLayout from "@layouts/defaultLayouts.astro"; import OnchainAnalytics from "@components/OnchainAnalytics"; const CONTRACT_ADDRESS = import.meta.env.PUBLIC_ANALYTICS_CONTRACT; const title = "Onchain Analytics - @iammatthias"; const description = "Homebrew analytics system built on anonymous session management with keccak256 hashes and blockchain"; const ogImage = "/onchain-analytics"; ---

Onchain Analytics

Recording started at block number 12315477 , and is no longer collecting data. The smart contracts are verified on Basescan.

This site briefly used a homebrew analytics system. It was built on an annonymous session management system that leveraged keccak256 hashes and the blockchain — you can read more it here.

================================================ FILE: src/pages/resume.astro ================================================ --- import DefaultLayout from "@layouts/defaultLayouts.astro"; const resumeData = [ { section: "Present", entries: [ { date: "2025 ➵ Current", company: "Pinata", position: "DevRel", }, { date: "2024 ➵ Current", company: "day---break", position: "Owner", }, { date: "2012 ➵ Current", company: "Photographer", position: "Fine art & freelance", }, ], }, { section: "Historic", entries: [ { date: "2023 ➵ 2024", company: "Ice Barrel (Contractor)", position: "Web Development & Performance Marketing Manager", }, { date: "2022 ➵ 2024", company: "Opul (Contractor)", position: "Design System Engineer", }, { date: "2021 ➵ 2022", company: "Tornado", position: "Growth Engineer", }, { date: "2018 ➵ 2021", company: "Aspiration", position: "CRM Architect", }, { date: "2016 ➵ 2018", company: "Surf Air", position: "Product & Marketing Coordinator", }, ], }, { section: "Education", entries: [ { date: "2010 ➵ 2014", company: "Brooks Institute", position: "Bachelors Degree, Commercial Photography", }, ], }, ]; const title = "Resume - @iammatthias"; const description = "Professional experience and education history"; const ogImage = "/resume"; --- { resumeData.map((section) => (

{section.section}

{section.entries.map((entry) => (

{entry.company}

{entry.position}

{entry.date}

))}
)) }
================================================ FILE: src/pages/robots.txt.ts ================================================ import type { APIRoute } from "astro"; const getRobotsTxt = (sitemapURL: URL) => ` User-agent: * Allow: / Sitemap: ${sitemapURL.href} `; export const GET: APIRoute = ({ site }) => { const sitemapURL = new URL("sitemap-index.xml", site); return new Response(getRobotsTxt(sitemapURL)); }; ================================================ FILE: src/pages/rss.xml.ts ================================================ import rss from "@astrojs/rss"; import { getCollection } from "astro:content"; import { getGitHubCollections, type GitHubContentData } from "@lib/github-loader"; import type { APIContext } from "astro"; type Entry = { id: string; data: GitHubContentData; collection: string; }; export async function GET(context: APIContext) { const GITHUB_OWNER = "iammatthias"; const GITHUB_REPO = "obsidian_cms"; const GITHUB_TOKEN = import.meta.env.GITHUB; const collectionNames = await getGitHubCollections(GITHUB_OWNER, GITHUB_REPO, GITHUB_TOKEN, "main", "content"); // Gather all entries from all collections const allEntries: Entry[] = []; for (const collectionName of collectionNames) { const entries = (await getCollection(collectionName as any)) as Entry[]; entries.forEach((entry) => { allEntries.push({ ...entry, collection: collectionName, }); }); } // Sort by created date (most recent first) allEntries.sort((a, b) => { const dateA = new Date(a.data.created || 0).getTime(); const dateB = new Date(b.data.created || 0).getTime(); return dateB - dateA; }); return rss({ title: "iammatthias", description: "All content from iammatthias.com", site: context.site?.toString() || "https://iammatthias.com", items: allEntries.map((entry) => ({ title: entry.data.title, pubDate: new Date(entry.data.created), description: entry.data.excerpt || "", link: `/content/${entry.collection}/${entry.id}/`, categories: entry.data.tags || [], })), customData: "en-us", stylesheet: "/rss.xml.xsl", }); } ================================================ FILE: src/pages/tags/[tag]/index.astro ================================================ --- import { getCollection } from "astro:content"; import DefaultLayout from "@layouts/defaultLayouts.astro"; import { getGitHubCollections, type GitHubContentData } from "@lib/github-loader"; import type { TagData } from "@lib/tags-loader"; type TagEntry = { id: string; data: TagData; }; type Entry = { id: string; data: GitHubContentData; collection: string; }; export async function getStaticPaths() { const tags = (await getCollection("tags")) as TagEntry[]; return tags.map((tag) => ({ params: { tag: tag.id }, props: { tag }, })); } interface Props { tag: TagEntry; } const { tag } = Astro.props; const tagName = Astro.params.tag; // Get all collections const GITHUB_OWNER = "iammatthias"; const GITHUB_REPO = "obsidian_cms"; const GITHUB_TOKEN = import.meta.env.GITHUB; const collectionNames = await getGitHubCollections(GITHUB_OWNER, GITHUB_REPO, GITHUB_TOKEN, "main", "content"); // Get all entries from all collections and filter by tag const entriesWithTag: Entry[] = []; for (const collectionName of collectionNames) { const entries = (await getCollection(collectionName as any)) as Entry[]; for (const entry of entries) { if (entry.data.tags?.includes(tagName as string)) { entriesWithTag.push({ ...entry, collection: collectionName, }); } } } // Sort by created date (most recent first) entriesWithTag.sort((a, b) => { const dateA = new Date(a.data.created || 0).getTime(); const dateB = new Date(b.data.created || 0).getTime(); return dateB - dateA; }); const title = `#${tagName} - @iammatthias`; const description = `${entriesWithTag.length} ${entriesWithTag.length === 1 ? "entry" : "entries"} tagged with ${tagName}`; const ogImage = `/og-tags-${encodeURIComponent(tagName as string)}.png`; ---

#{tagName}

{entriesWithTag.length} {entriesWithTag.length === 1 ? "entry" : "entries"}

{ entriesWithTag.map((entry) => (

{entry.data.title}

{entry.collection}

{entry.data.excerpt &&

{entry.data.excerpt}

}
)) }
================================================ FILE: src/pages/tags/index.astro ================================================ --- import { getCollection } from "astro:content"; import DefaultLayout from "@layouts/defaultLayouts.astro"; import type { TagData } from "@lib/tags-loader"; type TagEntry = { id: string; data: TagData; }; const tags = (await getCollection("tags")) as TagEntry[]; // Sort by count (most used first) tags.sort((a, b) => b.data.count - a.data.count); const title = "All Tags - @iammatthias"; const description = `Browse ${tags.length} tags`; const ogImage = "/tags"; ---

All Tags

{tags.length} tags

================================================ FILE: src/styles/globals.css ================================================ :root { /* Previous theme - commented out */ /* --color-background: #121113; --color-foreground: #ffffff; --color-caret: #ffffff; --color-invisibles: #333333; --color-line-highlight: #22222255; --color-selection: #222222; --color-selection-foreground: #000000; --color-comment: #888888; --color-string: #fbcb97; --color-number: #e78a53; --color-constant: #e78a53; --color-variable: #c1c1c1; --color-keyword: #5f8787; --color-storage: #5f8787; --color-storage-type: #aaaaaa; --color-class: #fbcb97; --color-function: #aaaaaa; --color-parameter: #999999; --color-tag: #5f8787; --color-attribute: #999999; --color-punctuation: #ffffff; --color-text: #c1c1c1; --color-language-literal: #999999; --color-invalid: #5f8787; --color-invalid-deprecated: #e78a53; --color-deleted: #5f8787; --color-inserted: #fbcb97; --color-changed: #e78a53; --color-ignored: #888888; --color-untracked: #333333; --color-border: var(--color-string); --color-surface: #1a1819; --color-muted: var(--color-comment); --color-accent: var(--color-string); --color-accent-secondary: var(--color-number); */ /* Mediterranean Night Sailboat Theme */ /* Base theme colors */ --color-background: #0f1419; --color-background: color(display-p3 0.06 0.08 0.1); --color-foreground: #f1faee; --color-foreground: color(display-p3 0.95 0.98 0.94); --color-caret: #ffb800; --color-caret: color(display-p3 1 0.75 0); --color-invisibles: #1b4965; --color-invisibles: color(display-p3 0.1 0.3 0.42); --color-line-highlight: #1a1d2355; --color-line-highlight: color(display-p3 0.1 0.11 0.14 / 0.33); --color-selection: #1b4965; --color-selection: color(display-p3 0.1 0.3 0.42); --color-selection-foreground: #fefae0; --color-selection-foreground: color(display-p3 1 0.98 0.88); /* Syntax colors */ --color-comment: #6b8ca3; --color-comment: color(display-p3 0.42 0.55 0.64); --color-string: #ffb800; --color-string: color(display-p3 1 0.75 0); --color-number: #ff5733; --color-number: color(display-p3 1 0.35 0.2); --color-constant: #ff5733; --color-constant: color(display-p3 1 0.35 0.2); --color-variable: #d4d4d4; --color-variable: color(display-p3 0.84 0.84 0.84); --color-keyword: #2a6f97; --color-keyword: color(display-p3 0.16 0.45 0.62); --color-storage: #2a6f97; --color-storage: color(display-p3 0.16 0.45 0.62); --color-storage-type: #a68a64; --color-storage-type: color(display-p3 0.66 0.56 0.42); --color-class: #ff9a3d; --color-class: color(display-p3 1 0.62 0.24); --color-function: #a68a64; --color-function: color(display-p3 0.66 0.56 0.42); --color-parameter: #8b9bb3; --color-parameter: color(display-p3 0.55 0.62 0.72); --color-tag: #2a6f97; --color-tag: color(display-p3 0.16 0.45 0.62); --color-attribute: #8b9bb3; --color-attribute: color(display-p3 0.55 0.62 0.72); --color-punctuation: #f1faee; --color-punctuation: color(display-p3 0.95 0.98 0.94); --color-text: #d4d4d4; --color-text: color(display-p3 0.84 0.84 0.84); --color-language-literal: #8b9bb3; --color-language-literal: color(display-p3 0.55 0.62 0.72); /* Status colors */ --color-invalid: #2a6f97; --color-invalid: color(display-p3 0.16 0.45 0.62); --color-invalid-deprecated: #ff5733; --color-invalid-deprecated: color(display-p3 1 0.35 0.2); --color-deleted: #2a6f97; --color-deleted: color(display-p3 0.16 0.45 0.62); --color-inserted: #ffb800; --color-inserted: color(display-p3 1 0.75 0); --color-changed: #ff5733; --color-changed: color(display-p3 1 0.35 0.2); --color-ignored: #6b8ca3; --color-ignored: color(display-p3 0.42 0.55 0.64); --color-untracked: #1b4965; --color-untracked: color(display-p3 0.1 0.3 0.42); /* Semantic colors derived from theme */ --color-border: var(--color-string); --color-surface: #1a1d23; --color-surface: color(display-p3 0.1 0.11 0.14); --color-muted: var(--color-comment); --color-accent: var(--color-string); --color-accent-secondary: var(--color-number); } body { font-size: 16px; padding: 1rem; padding-top: calc(env(safe-area-inset-top) + 1rem); padding-right: calc(env(safe-area-inset-right) + 1rem); padding-bottom: calc(env(safe-area-inset-bottom) + 1rem); padding-left: calc(env(safe-area-inset-left) + 1rem); font-family: -apple-system-ui-serif, ui-serif, "Georgia", serif; background-color: var(--color-background); color: var(--color-foreground); caret-color: var(--color-caret); } ::selection { background-color: var(--color-selection); color: var(--color-selection-foreground); } ::-moz-selection { background-color: var(--color-selection); color: var(--color-selection-foreground); } /* Typography - Content styles for markdown rendering */ h1, h2, h3, h4, h5, h6 { line-height: 1.2; font-weight: 700; } h1 { font-size: 3.5em; } h2 { font-size: 1.75rem; } h3 { font-size: 1.5rem; } h4 { font-size: 1.25rem; } h5 { font-size: 1.1rem; } h6 { font-size: 1rem; } p { line-height: 1.6; } a { color: inherit; text-decoration: underline; } a:hover { opacity: 0.7; } ol, ul { list-style: none; display: flex; flex-direction: column; gap: 1rem; margin-left: 2rem; } ol { list-style: decimal; } ul { list-style: disc; } li { font-family: ui-monospace, SFMono-Regular, ui-monospace, Monaco, "Andale Mono", "Ubuntu Mono", monospace; font-size: 0.8rem; } blockquote { padding: 0.5rem 0.75rem; border-left: 4px solid var(--color-border); font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; } code, pre, table { font-family: ui-monospace, SFMono-Regular, ui-monospace, Monaco, "Andale Mono", "Ubuntu Mono", monospace; font-size: 0.75rem; } code { font-size: 0.9em; padding: 0.2rem 0.4rem; background: var(--color-surface); } pre { padding: 1.5rem; @media (max-width: 768px) { padding: 1rem; } background: var(--color-surface); overflow-x: auto; } pre code { padding: 0; background: transparent; } table { width: 100%; border-collapse: collapse; } th, td { padding: 0.25rem; border: 1px solid var(--color-border); text-align: left; vertical-align: top; } th { background: var(--color-surface); } img { max-width: 100%; height: auto; } hr { border: none; border-top: 1px solid var(--color-border); } figcaption { font-size: 0.9rem; color: var(--color-muted); text-align: center; } .heavy { font-weight: 900; } .tag { font-family: ui-monospace, SFMono-Regular, ui-monospace, Monaco, "Andale Mono", "Ubuntu Mono", monospace; font-size: 0.6rem; } .muted { color: var(--color-muted); } .muted p { font-size: 0.9rem; } ================================================ FILE: src/styles/reset.css ================================================ /* Box sizing rules */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; text-wrap: pretty; } /* Prevent font size inflation */ html { -moz-text-size-adjust: none; -webkit-text-size-adjust: none; text-size-adjust: none; } /* Remove default margin in favour of better control in authored CSS */ body, h1, h2, h3, h4, p, figure, blockquote, dl, dd { margin-block-end: 0; } /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ ul[role="list"], ol[role="list"] { list-style: none; } /* Set core body defaults */ body { min-height: 100dvh; line-height: 1.5; } /* Set shorter line heights on headings and interactive elements */ h1, h2, h3, h4, button, input, label { line-height: 1.1; } /* Balance text wrapping on headings */ h1, h2, h3, h4 { text-wrap: balance; } /* A elements that don't have a class get default styles */ a:not([class]) { text-decoration-skip-ink: auto; color: currentColor; } /* Make images easier to work with */ img, picture { max-width: 100%; display: block; } /* Inherit fonts for inputs and buttons */ input, button, textarea, select { font-family: inherit; font-size: inherit; } /* Make sure textareas without a rows attribute are not tiny */ textarea:not([rows]) { min-height: 10em; } /* Anything that has been anchored to should have extra scroll margin */ :target { scroll-margin-block: 5ex; } ================================================ FILE: tsconfig.json ================================================ { "extends": "astro/tsconfigs/strict", "include": [".astro/types.d.ts", "**/*"], "exclude": ["dist", ".mastra"], "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "react", "paths": { "@src/*": ["./src/*"], "@styles/*": ["./src/styles/*"], "@components/*": ["./src/components/*"], "@layouts/*": ["./src/layouts/*"], "@pages/*": ["./src/pages/*"], "@mastra/*": ["./src/mastra/*"], "@actions/*": ["./src/actions/*"], "@lib/*": ["./src/lib/*"] } } } ================================================ FILE: wrangler.toml ================================================ name = "com" main = "@astrojs/cloudflare/entrypoints/server" compatibility_date = "2024-09-23" compatibility_flags = ["nodejs_compat"] # Session storage. wrangler auto-provisions a KV namespace on first deploy. # After the first successful deploy, run `wrangler kv namespace list`, copy # the id, and add `id = "..."` below so future deploys reuse it. [[kv_namespaces]] binding = "SESSION" # IMAGES binding is added automatically by @astrojs/cloudflare. # # All env values used by this site are read at *build time* via # `import.meta.env.*` (Vite inlines them into the bundle), so they belong # in the dashboard's Build environment, NOT in [vars] here. The runtime # Worker doesn't need any user-set env vars. # # Required Build env (Dashboard -> Worker -> Settings -> Build -> # "Build variables and secrets"): # - GITHUB (Secret) GitHub PAT for obsidian_cms repo # - PUBLIC_ANALYTICS_CONTRACT (Variable) 0x29867a886F96e84196fa47DEe2Fb4a5853412C03