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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" encoding="UTF-8" indent="yes"/>
<xsl:template match="/rss/channel">
<html>
<head>
<title><xsl:value-of select="title"/> - RSS Feed</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<!-- Open Graph / Social Media -->
<meta property="og:type" content="website"/>
<meta property="og:title" content="{title} - RSS Feed"/>
<meta property="og:description" content="{description}"/>
<meta property="og:image" content="https://og.iammatthias.com/"/>
<meta property="og:url" content="{link}/rss.xml"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:title" content="{title} - RSS Feed"/>
<meta name="twitter:description" content="{description}"/>
<meta name="twitter:image" content="https://og.iammatthias.com/RSS"/>
<meta name="theme-color" content="#0F1419"/>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-size: 16px;
padding: 1rem;
font-family: -apple-system-ui-serif, ui-serif, "Georgia", serif;
background-color: #0f1419;
background-color: color(display-p3 0.06 0.08 0.1);
color: #f1faee;
color: color(display-p3 0.95 0.98 0.94);
caret-color: #ffb800;
caret-color: color(display-p3 1 0.75 0);
line-height: 1.6;
}
::selection {
background-color: #1b4965;
background-color: color(display-p3 0.1 0.3 0.42);
color: #fefae0;
color: color(display-p3 1 0.98 0.88);
}
::-moz-selection {
background-color: #1b4965;
background-color: color(display-p3 0.1 0.3 0.42);
color: #fefae0;
color: color(display-p3 1 0.98 0.88);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.2;
font-weight: 700;
}
h1 {
font-size: 3.5em;
margin-bottom: 0.5rem;
}
h2 {
font-size: 1.5rem;
margin: 0 0 0.5rem 0;
}
a {
color: inherit;
text-decoration: underline;
}
a:hover {
opacity: 0.7;
}
.header {
margin-bottom: 2rem;
}
.header-meta {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
}
.header-meta p {
color: #6b8ca3;
color: color(display-p3 0.42 0.55 0.64);
margin: 0;
font-size: 0.9rem;
}
.rss-link {
font-size: 0.85rem;
text-decoration: none;
color: inherit;
border: 1px solid #ffb800;
border: 1px solid color(display-p3 1 0.75 0);
padding: 0.25rem 0.5rem;
font-family: ui-monospace, SFMono-Regular, ui-monospace, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
}
.rss-link:hover {
background: #1a1d23;
background: color(display-p3 0.1 0.11 0.14);
opacity: 1;
}
.items {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.item {
border: 1px solid #ffb800;
border: 1px solid color(display-p3 1 0.75 0);
padding: 1.5rem;
}
.item h2 a {
text-decoration: none;
}
.item h2 a:hover {
text-decoration: underline;
}
.item-meta {
font-family: ui-monospace, SFMono-Regular, ui-monospace, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
font-size: 0.9rem;
color: #6b8ca3;
color: color(display-p3 0.42 0.55 0.64);
margin-bottom: 1rem;
}
.item-description {
line-height: 1.6;
}
.footer {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid #ffb800;
border-top: 1px solid color(display-p3 1 0.75 0);
display: flex;
flex-wrap: wrap;
gap: 0;
}
.footer > * {
border-right: 1px solid #ffb800;
border-right: 1px solid color(display-p3 1 0.75 0);
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
font-family: ui-monospace, SFMono-Regular, ui-monospace, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
}
code {
font-family: ui-monospace, SFMono-Regular, ui-monospace, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
font-size: 0.9em;
padding: 0.2rem 0.4rem;
background: #1a1d23;
background: color(display-p3 0.1 0.11 0.14);
}
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.item {
padding: 1rem;
}
}
@media (max-width: 640px) {
body {
padding: 0.5rem;
}
.header h1 {
font-size: 2.5em;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1><xsl:value-of select="title"/></h1>
<div class="header-meta">
<p><xsl:value-of select="description"/></p>
<a href="{link}" class="rss-link">View Site →</a>
</div>
</div>
<div class="items">
<xsl:apply-templates select="item"/>
</div>
<div class="footer">
<p>Last Updated: <xsl:value-of select="lastBuildDate"/></p>
<p>Subscribe by copying the URL into your RSS reader</p>
</div>
</div>
</body>
</html>
</xsl:template>
<xsl:template match="item">
<article class="item">
<h2>
<a href="{link}">
<xsl:value-of select="title"/>
</a>
</h2>
<div class="item-meta">
<xsl:value-of select="pubDate"/>
</div>
<div class="item-description">
<xsl:value-of select="description"/>
</div>
</article>
</xsl:template>
</xsl:stylesheet>
================================================
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<THREE.Mesh>(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 (
<mesh ref={meshRef} position={[0, 0, 0]} scale={[viewport.width, viewport.height, 1]}>
<primitive object={geometry} attach='geometry' />
<primitive object={material} attach='material' />
</mesh>
);
}
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 (
<div
style={{
position: "fixed",
top: 54,
left: 17,
right: 17,
bottom: 54,
zIndex: -1,
pointerEvents: "none",
}}
>
<Canvas
camera={{ position: [0, 0, 1], fov: 60 }}
dpr={dpr}
style={{ width: "100%", height: "100%", display: "block" }}
resize={{ scroll: false }}
>
<BlackHoleShader seed={seed} iterations={iterations} />
</Canvas>
</div>
);
}
================================================
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 (
<a
href={embed.fullsize}
target="_blank"
rel="noopener noreferrer"
className="bsky-image"
>
<img src={embed.thumb} alt={embed.alt} loading="lazy" />
</a>
);
}
function VideoEmbed({ embed }: { embed: NormalizedVideo }) {
return (
<div className="bsky-video">
<video controls preload="metadata">
<source src={embed.url} type={embed.mimeType} />
Your browser does not support this video format.
</video>
{embed.alt && <span className="bsky-video-alt">{embed.alt}</span>}
</div>
);
}
function ExternalEmbed({ embed }: { embed: NormalizedExternal }) {
return (
<a
href={embed.uri}
target="_blank"
rel="noopener noreferrer"
className="bsky-external"
>
{embed.thumb && (
<img
src={embed.thumb}
alt=""
className="bsky-external-thumb"
loading="lazy"
/>
)}
<div className="bsky-external-content">
<span className="bsky-external-title">{embed.title}</span>
<span className="bsky-external-description">{embed.description}</span>
<span className="bsky-external-uri">{new URL(embed.uri).hostname}</span>
</div>
</a>
);
}
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 (
<a
href={`https://bsky.app/profile/${did}/post/${postId}`}
target="_blank"
rel="noopener noreferrer"
className="bsky-quote"
>
<span className="bsky-quote-label">Quoted post</span>
</a>
);
}
function EmbedRenderer({ embed }: { embed: NormalizedEmbed }) {
switch (embed.type) {
case "image":
return <ImageEmbed embed={embed} />;
case "video":
return <VideoEmbed embed={embed} />;
case "external":
return <ExternalEmbed embed={embed} />;
case "quote":
return <QuoteEmbed embed={embed} />;
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 (
<article className="bsky-post">
<a
href={post.postUrl}
target="_blank"
rel="noopener noreferrer"
className="bsky-post-link"
>
<time className="bsky-time" dateTime={post.createdAt}>
{formatDate(post.createdAt)}
</time>
{post.text && <p className="bsky-text">{post.text}</p>}
</a>
{images.length > 0 && (
<div
className={`bsky-images bsky-images-${Math.min(images.length, 4)}`}
>
{images.map((img, i) => (
<ImageEmbed key={i} embed={img} />
))}
</div>
)}
{otherEmbeds.map((embed, i) => (
<EmbedRenderer key={i} embed={embed} />
))}
</article>
);
}
interface BlueskyFeedProps {
limit?: number;
}
export default function BlueskyFeed({ limit = 10 }: BlueskyFeedProps) {
const [posts, setPosts] = useState<BlueskyPost[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [errorMsg, setErrorMsg] = useState<string | null>(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 (
<div className="bsky-container">
<div className="bsky-loading">Loading posts from Bluesky...</div>
</div>
);
}
if (errorMsg) {
return (
<div className="bsky-container">
<div className="bsky-error">{errorMsg}</div>
</div>
);
}
if (posts.length === 0) {
return (
<div className="bsky-container">
<div className="bsky-empty">No posts found.</div>
</div>
);
}
return (
<div className="bsky-container">
<div className="bsky-feed">
{posts.map((post) => (
<PostCard key={post.cid} post={post} />
))}
</div>
<div className="bsky-view-all">
<a
href="https://bsky.app/profile/iammatthias.com"
target="_blank"
rel="noopener noreferrer"
>
View more on Bluesky →
</a>
</div>
</div>
);
}
================================================
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 (
<a href={url} target="_blank" rel="noopener noreferrer" className="fc-image">
<img src={url} alt="" loading="lazy" />
</a>
);
}
return (
<a href={url} target="_blank" rel="noopener noreferrer" className="fc-link">
{new URL(url).hostname}
</a>
);
}
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 (
<article className="fc-post">
<a href={post.postUrl} target="_blank" rel="noopener noreferrer" className="fc-post-link">
<time className="fc-time" dateTime={post.createdAt}>
{formatDate(post.createdAt)}
</time>
{post.text && <p className="fc-text">{post.text}</p>}
</a>
{imageEmbeds.length > 0 && (
<div className={`fc-images fc-images-${Math.min(imageEmbeds.length, 4)}`}>
{imageEmbeds.map((embed, i) => (
<UrlEmbed key={i} url={embed.url!} />
))}
</div>
)}
{otherEmbeds.map((embed, i) => {
if (embed.type === "url" && embed.url) {
return <UrlEmbed key={i} url={embed.url} />;
}
if (embed.type === "cast" && embed.castHash) {
const hashHex = embed.castHash.startsWith("0x") ? embed.castHash.slice(2) : embed.castHash;
return (
<a
key={i}
href={`https://warpcast.com/~/conversations/${hashHex}`}
target="_blank"
rel="noopener noreferrer"
className="fc-quote"
>
<span className="fc-quote-label">Quoted cast</span>
</a>
);
}
return null;
})}
</article>
);
}
interface FarcasterFeedProps {
limit?: number;
onDataLoaded?: (posts: FarcasterPost[]) => void;
}
export default function FarcasterFeed({ limit = 10, onDataLoaded }: FarcasterFeedProps) {
const [posts, setPosts] = useState<FarcasterPost[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [errorMsg, setErrorMsg] = useState<string | null>(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 (
<div className="fc-container">
<div className="fc-loading">Loading casts from Farcaster...</div>
</div>
);
}
if (errorMsg) {
return (
<div className="fc-container">
<div className="fc-error">{errorMsg}</div>
</div>
);
}
if (posts.length === 0) {
return null; // Return nothing if no posts (ephemeral behavior)
}
return (
<div className="fc-container">
<div className="fc-feed">
{posts.map((post) => (
<PostCard key={post.hash} post={post} />
))}
</div>
<div className="fc-view-all">
<a href="https://warpcast.com/iammatthias" target="_blank" rel="noopener noreferrer">
View more on Warpcast →
</a>
</div>
</div>
);
}
================================================
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<PageView[]>([]);
const [events, setEvents] = useState<Event[]>([]);
const [sessionCount, setSessionCount] = useState<bigint>(0n);
const [sessionIds, setSessionIds] = useState<`0x${string}`[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [errorMsg, setErrorMsg] = useState<string | null>(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 (
<div className='onchain-analytics-container'>
<div className='onchain-analytics-loading'>Loading analytics data...</div>
</div>
);
}
if (errorMsg) {
return (
<div className='onchain-analytics-container'>
<div className='onchain-analytics-error'>{errorMsg}</div>
</div>
);
}
return (
<div className='onchain-analytics-container'>
<p>
<span id='sessionCount'>{sessionCount.toString()}</span> unique sessions have been recorded.
</p>
<table className='onchain-analytics-table'>
<thead>
<tr>
<th>Top 5 pages</th>
<th>Views</th>
</tr>
</thead>
<tbody>
{pageViews.map((view, index) => (
<tr key={index}>
<td className='onchain-analytics-break'>{view.page}</td>
<td>{view.views}</td>
</tr>
))}
</tbody>
</table>
<table className='onchain-analytics-table'>
<thead>
<tr>
<th>Top events</th>
<th>Count</th>
</tr>
</thead>
<tbody>
{events.map((event, index) => (
<tr key={index}>
<td>{event.event}</td>
<td>{event.count}</td>
</tr>
))}
</tbody>
</table>
<h4>Session Hashes</h4>
<p>
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.
</p>
<p>Click on a hash to view its corresponding portrait:</p>
<div className='onchain-analytics-session-hashes'>
{sessionIds.map((sessionId, index) => (
<a
key={index}
href={`/onchain-analytics/${sessionId}`}
className='onchain-analytics-session-hash onchain-analytics-break'
>
{sessionId}
</a>
))}
</div>
</div>
);
}
================================================
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<GlassPhoto[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [errorMsg, setErrorMsg] = useState<string | null>(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 (
<div className='recent-glass-container'>
<h2>Recent from Glass</h2>
<div className='recent-glass-loading'>Loading photos from Glass...</div>
</div>
);
}
return (
<div className='recent-glass-container'>
<Masonry
items={photos}
config={{
columns: [2, 4, 6],
gap: [16, 16, 32],
media: [640, 768, 1024],
}}
render={(photo) => (
<a href={photo.share_url} target='_blank' rel='noopener noreferrer' className='glass-photo' key={photo.id}>
<img
src={photo.image640x640}
alt={photo.caption || "Photo from Glass"}
width={photo.width}
height={photo.height}
loading='lazy'
/>
<div className='overlay'>
<div className='exif-info'>
{photo.exif?.camera && <div className='exif-line'>{photo.exif.camera}</div>}
{photo.exif?.lens && <div className='exif-line'>{photo.exif.lens}</div>}
{(photo.exif?.aperture || photo.exif?.focal_length || photo.exif?.iso || photo.exif?.exposure_time) && (
<div className='exif-line'>
{[photo.exif.aperture, photo.exif.focal_length, photo.exif.iso, photo.exif.exposure_time]
.filter(Boolean)
.join(" · ")}
</div>
)}
{photo.caption && <div className='caption-text'>{photo.caption}</div>}
</div>
</div>
</a>
)}
/>
<div className='view-all'>
<a href='https://glass.photo/iam' target='_blank' rel='noopener noreferrer'>
View more on Glass →
</a>
</div>
</div>
);
}
================================================
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<T extends BasePost>(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 (
<article className="social-post">
<a
href={post.postUrl}
target="_blank"
rel="noopener noreferrer"
className="social-post-link"
>
<time className="social-time" dateTime={post.createdAt}>
{formatDate(post.createdAt)}
</time>
{post.text && <p className="social-text">{post.text}</p>}
</a>
{images.length > 0 && (
<div
className={`social-images social-images-${Math.min(images.length, 4)}`}
>
{images.map((img, i) => (
<a
key={i}
href={img.fullsize}
target="_blank"
rel="noopener noreferrer"
className="social-image"
>
<img src={img.thumb} alt={img.alt} loading="lazy" />
</a>
))}
</div>
)}
{otherEmbeds.map((embed, i) => {
if (embed.type === "video") {
return (
<div key={i} className="social-video">
<video controls preload="metadata">
<source src={embed.url} type={embed.mimeType} />
</video>
</div>
);
}
if (embed.type === "external") {
return (
<a
key={i}
href={embed.uri}
target="_blank"
rel="noopener noreferrer"
className="social-external"
>
{embed.thumb && (
<img
src={embed.thumb}
alt=""
className="social-external-thumb"
loading="lazy"
/>
)}
<div className="social-external-content">
<span className="social-external-title">{embed.title}</span>
<span className="social-external-uri">
{new URL(embed.uri).hostname}
</span>
</div>
</a>
);
}
if (embed.type === "quote") {
const uriParts = embed.uri.split("/");
const postId = uriParts[uriParts.length - 1];
const did = uriParts[2];
return (
<a
key={i}
href={`https://bsky.app/profile/${did}/post/${postId}`}
target="_blank"
rel="noopener noreferrer"
className="social-quote"
>
Quoted post
</a>
);
}
return null;
})}
</article>
);
}
// 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 (
<article className="social-post">
<a
href={post.postUrl}
target="_blank"
rel="noopener noreferrer"
className="social-post-link"
>
<time className="social-time" dateTime={post.createdAt}>
{formatDate(post.createdAt)}
</time>
{post.text && <p className="social-text">{post.text}</p>}
</a>
{imageEmbeds.length > 0 && (
<div
className={`social-images social-images-${Math.min(imageEmbeds.length, 4)}`}
>
{imageEmbeds.map((embed, i) => (
<a
key={i}
href={embed.url}
target="_blank"
rel="noopener noreferrer"
className="social-image"
>
<img src={embed.url} alt="" loading="lazy" />
</a>
))}
</div>
)}
{otherEmbeds.map((embed, i) => {
if (embed.type === "url" && embed.url) {
return (
<a
key={i}
href={embed.url}
target="_blank"
rel="noopener noreferrer"
className="social-link"
>
{new URL(embed.url).hostname}
</a>
);
}
if (embed.type === "cast" && embed.castHash) {
const hashHex = embed.castHash.startsWith("0x")
? embed.castHash.slice(2)
: embed.castHash;
return (
<a
key={i}
href={`https://warpcast.com/~/conversations/${hashHex}`}
target="_blank"
rel="noopener noreferrer"
className="social-quote"
>
Quoted cast
</a>
);
}
return null;
})}
</article>
);
}
// Glass post card
function GlassPostCard({ post }: { post: GlassPost }) {
return (
<article className="social-post social-post-glass">
<a
href={post.postUrl}
target="_blank"
rel="noopener noreferrer"
className="social-post-link"
>
<time className="social-time" dateTime={post.createdAt}>
{formatDate(post.createdAt)}
</time>
</a>
<a
href={post.postUrl}
target="_blank"
rel="noopener noreferrer"
className="social-image"
>
<img
src={post.image640x640}
alt={post.caption || "Photo from Glass"}
loading="lazy"
width={post.width}
height={post.height}
/>
</a>
{post.caption && <p className="social-text">{post.caption}</p>}
{post.exif && (
<div className="social-exif">
{post.exif.camera && <span>{post.exif.camera}</span>}
{post.exif.lens && <span>{post.exif.lens}</span>}
{(post.exif.aperture ||
post.exif.focal_length ||
post.exif.iso ||
post.exif.exposure_time) && (
<span>
{[
post.exif.aperture,
post.exif.focal_length,
post.exif.iso,
post.exif.exposure_time,
]
.filter(Boolean)
.join(" · ")}
</span>
)}
</div>
)}
</article>
);
}
interface SocialFeedsProps {
limit?: number;
}
export default function SocialFeeds({ limit = 10 }: SocialFeedsProps) {
const [blueskyPosts, setBlueskyPosts] = useState<BlueskyPost[]>([]);
const [farcasterPosts, setFarcasterPosts] = useState<FarcasterPost[]>([]);
const [glassPosts, setGlassPosts] = useState<GlassPost[]>([]);
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: () => (
<details className="social-column" open key="bluesky">
<summary>Bluesky</summary>
<div className="social-feed">
{freshBlueskyPosts.map((post) => (
<BlueskyPostCard key={post.cid} post={post} />
))}
</div>
<div className="social-view-all">
<a
href="https://bsky.app/profile/iammatthias.com"
target="_blank"
rel="noopener noreferrer"
>
View more on Bluesky →
</a>
</div>
</details>
),
},
{
name: "farcaster",
active: farcasterActive,
mostRecent: freshFarcasterPosts[0]
? new Date(freshFarcasterPosts[0].createdAt).getTime()
: 0,
render: () => (
<details className="social-column" open key="farcaster">
<summary>Farcaster</summary>
<div className="social-feed">
{freshFarcasterPosts.map((post) => (
<FarcasterPostCard key={post.hash} post={post} />
))}
</div>
<div className="social-view-all">
<a
href="https://warpcast.com/iammatthias"
target="_blank"
rel="noopener noreferrer"
>
View more on Warpcast →
</a>
</div>
</details>
),
},
{
name: "glass",
active: glassActive,
mostRecent: freshGlassPosts[0]
? new Date(freshGlassPosts[0].createdAt).getTime()
: 0,
render: () => (
<details className="social-column" open key="glass">
<summary>Glass</summary>
<div className="social-feed">
{freshGlassPosts.map((post) => (
<GlassPostCard key={post.id} post={post} />
))}
</div>
<div className="social-view-all">
<a
href="https://glass.photo/iam"
target="_blank"
rel="noopener noreferrer"
>
View more on Glass →
</a>
</div>
</details>
),
},
];
// 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 <div className="social-loading">Loading social feeds...</div>;
}
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 (
<div className={`social-feeds ${gridClass}`}>
{activeFeeds.map((feed) => feed.render())}
</div>
);
}
================================================
FILE: src/components/footer.astro
================================================
---
---
<footer>
<p><a href="/now">Now</a></p>
<p><a href="/tags">Tags</a></p>
<p><a href="/rss.xml">RSS</a></p>
<p><a href="/sitemap-index.xml">Sitemap</a></p>
</footer>
<style>
footer {
border: 1px solid var(--color-border);
display: flex;
flex-wrap: wrap;
flex-direction: row;
margin-top: auto;
position: sticky;
bottom: 1rem;
background: var(--color-background);
z-index: 1000;
}
footer > * {
border-right: 1px solid var(--color-border);
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
width: fit-content;
}
</style>
================================================
FILE: src/components/homepage/heroSection.astro
================================================
---
---
<section>
<h1>Hi, I am Matthias</h1>
<p>
I run DevRel at <a href='https://www.pinata.cloud/' title='Pinata'>Pinata</a> and have a small growth consustancy called
<a href='https://day---break.com/' title='day---break'>day---break</a> on the side.
</p>
<p>Over the years I've worked as a photographer, and have built growth teams in travel, fintech, and ecommerce.</p>
<p>
This site is a collection notes, recipews, and thoughts. You'll find projects, ongoing and final, and recomendations
for all sorts of things.
</p>
<p>
You can find me on various <a href='/social'>social</a> platforms, subscribe to my <a href='/rss.xml'>rss feed</a>,
or find me on the small-web via <a href='https://github.com/iammatthias/gemini-server'>Gemini</a>: <code
>gemini://gem.iammatthias.com</code
>
</p>
</section>
<style>
section {
padding: 2rem;
@media (max-width: 768px) {
padding: 1rem;
}
min-height: calc(50vh - 2rem);
display: flex;
flex-direction: column;
gap: 1rem;
}
section > * {
max-width: 64ch;
word-wrap: pretty;
}
</style>
================================================
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
});
---
<section>
<div class='grid'>
{
collectionsWithContent.map((collection) => (
<div class='collection-section'>
<h3>
<a href={`/content/${collection.name}`}>{collection.name}</a>
</h3>
<ul>
{collection.recent.map((entry) => (
<li>
<a href={`/content/${collection.name}/${entry.id}`}>{entry.data.title}</a>
<br />
<span class='date'>{new Date(entry.data.created).toLocaleDateString()}</span>
</li>
))}
<li>
<a href={`/content/${collection.name}`}>View all →</a>
</li>
</ul>
</div>
))
}
</div>
{
collectionsWithContent.length > 0 && (
<div class='view-all'>
<a href='/content'>View all content →</a>
</div>
)
}
</section>
<style>
section {
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;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
@media (max-width: 768px) {
gap: 1rem;
}
}
.collection-section {
border: 1px solid var(--color-border);
padding: 1.5rem;
@media (max-width: 768px) {
padding: 1rem;
}
}
.collection-section h3 {
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
}
.collection-section h3 a {
text-decoration: none;
color: inherit;
text-transform: capitalize;
}
.collection-section h3 a:hover {
text-decoration: underline;
}
.collection-section ul {
list-style: none;
padding: 0;
margin: 0;
}
.collection-section ul li {
padding: 0.5rem 0;
border-bottom: 1px solid var(--color-border);
}
.collection-section ul li:last-child {
border-bottom: none;
}
.collection-section ul li a {
text-decoration: none;
color: inherit;
}
.collection-section ul li a:hover {
text-decoration: underline;
}
.view-all {
text-align: center;
}
.view-all a {
text-decoration: none;
color: inherit;
font-weight: 500;
}
.view-all a:hover {
text-decoration: underline;
}
.date {
color: var(--color-muted);
font-size: 0.85rem;
}
</style>
================================================
FILE: src/components/homepage/recentFeeds.astro
================================================
---
---
<section>
<h2>feeds</h2>
<p>blogs I like that have rss</p>
<div class='grid'>
<ul><li>item 1</li><li>item 2</li><li>item 3</li></ul>
</div>
</section>
<style>
section {
margin-top: auto;
min-height: calc(50vh - 2rem);
border-top: 1px solid var(--color-border);
padding: 2rem;
@media (max-width: 768px) {
padding: 1rem;
}
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
grid-wrap: wrap;
}
.grid > ul {
list-style: none;
padding: 0;
margin: 0;
}
.grid > ul > li {
padding: 0.5rem;
border-bottom: 1px solid var(--color-border);
&:last-child {
border-bottom: none;
}
}
</style>
================================================
FILE: src/components/nav.astro
================================================
---
---
<nav>
<p><a href="/">Home</a></p>
<p><a href="/content">Content</a></p>
<p><a href="/about">About</a></p>
<p><a href="/now">Now</a></p>
<p><span id="time"></span></p>
</nav>
<style>
nav {
border: 1px solid var(--color-border);
display: flex;
flex-wrap: wrap;
flex-direction: row;
position: sticky;
top: 1rem;
background: var(--color-background);
z-index: 1000;
}
nav > * {
border-right: 1px solid var(--color-border);
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
width: fit-content;
&:last-child {
border-right: none;
border-left: 1px solid var(--color-border);
margin-left: auto;
}
}
</style>
<script>
// just time, no date. hour and minute. update every minute on the minue. hide seconds.
const time = document.getElementById("time")!;
time.innerHTML = new Date().toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
});
setInterval(() => {
time.innerHTML = new Date().toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
});
}, 60000);
</script>
================================================
FILE: src/components/sidebar/index.astro
================================================
---
---
<details id='sidebar' class='sidebar' open>
<summary class='sidebar-toggle' aria-label='Toggle sidebar'>
<svg width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'>
<path d='M3 6h18M3 12h18M3 18h18'></path>
</svg>
</summary>
<div class='sidebar-content'>
<slot />
</div>
</details>
<style is:global>
.sidebar {
overflow: hidden;
border: 1px solid var(--color-border);
display: flex;
flex-direction: column;
}
/* Desktop styles */
@media (min-width: 800px) {
.sidebar {
flex-basis: calc(50ch - 32px);
flex-grow: 1;
max-height: calc(100dvh - 2rem);
position: sticky;
top: 1rem;
padding: 0;
transition:
flex-basis 0.3s cubic-bezier(0.4, 0, 0.2, 1),
min-width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
max-width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar.collapsed {
flex-basis: 2.5rem;
flex-grow: 0;
min-width: 2.5rem;
max-width: 2.5rem;
}
.sidebar-content {
padding: 1rem;
opacity: 1;
visibility: visible;
margin-top: auto;
transition:
opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0.1s,
padding 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar.collapsed .sidebar-content {
opacity: 0;
visibility: hidden;
pointer-events: none;
padding: 0;
transition:
opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
padding 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0.1s;
}
}
/* Mobile styles */
@media (max-width: 799px) {
.sidebar {
position: fixed;
bottom: 1rem;
left: 1rem;
right: 1rem;
background: var(--color-background);
z-index: 1000;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
}
.sidebar:not([open]) {
height: auto;
}
.sidebar[open] {
height: calc(100dvh - 2rem);
display: flex;
flex-direction: column;
}
.sidebar-toggle {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0.5rem;
min-height: 2.5rem;
list-style: none;
flex-shrink: 0;
}
.sidebar-toggle::-webkit-details-marker {
display: none;
}
.sidebar-toggle svg {
margin: 0;
}
.sidebar-content {
padding: 0 1rem 1rem 1rem;
overflow-y: auto;
flex: 1;
}
.sidebar:not([open]) .sidebar-content {
display: none;
}
}
@media (min-width: 800px) {
.sidebar-toggle {
cursor: pointer;
}
}
.sidebar-toggle {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: none;
border: 1px solid var(--color-border);
cursor: pointer;
padding: 0.25rem;
color: var(--color-foreground);
transition: background-color 0.2s ease;
z-index: 10;
width: 1.5rem;
height: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.sidebar-toggle:hover {
background-color: var(--color-surface);
}
</style>
<script is:inline>
// Apply collapsed state immediately to prevent flash (desktop only)
(function () {
const isDesktop = window.innerWidth >= 800;
if (isDesktop) {
const savedState = localStorage.getItem("sidebar-collapsed");
const isCollapsed = savedState === "true";
if (isCollapsed) {
const style = document.createElement("style");
style.textContent =
".sidebar { flex-basis: 2.5rem !important; min-width: 2.5rem !important; max-width: 2.5rem !important; flex-grow: 0 !important; } .sidebar-content { opacity: 0 !important; visibility: hidden !important; padding: 0 !important; }";
document.head.appendChild(style);
document.addEventListener("DOMContentLoaded", function () {
setTimeout(function () {
style.remove();
}, 0);
});
}
}
})();
</script>
<script>
document.addEventListener("DOMContentLoaded", () => {
const sidebar = document.getElementById("sidebar") as HTMLDetailsElement;
const toggleButton = document.querySelector(".sidebar-toggle") as HTMLElement;
if (!sidebar || !toggleButton) return;
const isDesktop = window.innerWidth >= 800;
if (isDesktop) {
// Desktop: use class-based toggle and prevent details behavior
const savedState = localStorage.getItem("sidebar-collapsed");
const isCollapsed = savedState === "true";
if (isCollapsed) {
sidebar.classList.add("collapsed");
}
toggleButton.addEventListener("click", (e) => {
e.preventDefault();
sidebar.classList.toggle("collapsed");
const isNowCollapsed = sidebar.classList.contains("collapsed");
localStorage.setItem("sidebar-collapsed", isNowCollapsed.toString());
});
}
// Mobile: native details/summary behavior - no JS needed
});
</script>
================================================
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");
---
<html lang='en'>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1, viewport-fit=cover' />
<meta name='color-scheme' content='light dark' />
<meta name='theme-color' content='#0f1419' />
<title>{title}</title>
<meta name='description' content={description} />
<link rel='canonical' href={canonicalURL} />
<!-- Open Graph / Facebook -->
<meta property='og:type' content='website' />
<meta property='og:url' content={canonicalURL} />
<meta property='og:title' content={title} />
<meta property='og:description' content={description} />
<meta property='og:image' content={ogImageURL} />
<!-- Twitter -->
<meta property='twitter:card' content='summary_large_image' />
<meta property='twitter:url' content={canonicalURL} />
<meta property='twitter:title' content={title} />
<meta property='twitter:description' content={description} />
<meta property='twitter:image' content={ogImageURL} />
<link rel='alternate' type='application/rss+xml' title='iammatthias - All Content' href='/rss.xml' />
<link rel='sitemap' href='/sitemap-index.xml' />
<link
rel='icon'
href='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>☼</text></svg>'
/>
</head>
<body>
<div class='with-sidebar'>
<main>
<Nav />
<slot name='main' />
<Footer />
</main>
<slot name='sidebar' />
</div>
<div class='veil' aria-hidden='true'>
<div class='veil-overlay'></div>
</div>
</body>
</html>
<style>
.with-sidebar {
display: flex;
flex-wrap: wrap;
gap: 1rem;
min-height: calc(100dvh - 2rem);
&:before {
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
box-shadow:
inset 0px 0px 0px 16px var(--color-background),
inset 0px 0px 0px 17px var(--color-caret);
pointer-events: none;
z-index: 9999;
}
}
.with-sidebar > :first-child {
flex-basis: 0;
flex-grow: 999;
min-inline-size: 50%;
display: flex;
flex-direction: column;
}
/* .with-sidebar > * {
border: 1px solid var(--color-border);
} */
main > :not(nav):not(footer) {
flex: 1;
}
.veil {
display: none;
}
@supports (-webkit-touch-callout: none) {
/* .with-sidebar:before {
display: none;
} */
.veil {
position: sticky;
top: 0;
z-index: 10000;
display: block;
pointer-events: none;
}
.veil-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
backdrop-filter: blur(1px);
}
}
</style>
================================================
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<typeof githubContentSchema>;
// ============================================================================
// CACHE SYSTEM - Single fetch, shared across all loaders
// ============================================================================
interface CachedFile {
name: string;
content: string;
frontmatter: Record<string, any>;
body: string;
}
interface CachedCollection {
name: string;
files: CachedFile[];
}
interface ContentCache {
collections: CachedCollection[];
collectionNames: string[];
tagMap: Map<string, Set<string>>;
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<void> {
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<string, Set<string>> {
return contentCache.tagMap;
}
/**
* Ensure cache is populated
*/
export async function ensureCachePopulated(
owner: string,
repo: string,
token: string,
branch = "main",
contentPath = "content",
): Promise<void> {
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<string, any>;
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<string, any> {
const result: Record<string, any> = {};
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<string[]> {
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<Uint8Array> {
// 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<typeof tagsSchema>;
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";
---
<DefaultLayout title={title} description={description} ogImage={ogImage}>
<Fragment slot='main'>
<section>
<BlackHole seed={seed} client:load />
<h1>404</h1>
<p>Page not found</p>
<a href='/'>Return home</a>
</section>
</Fragment>
</DefaultLayout>
<style>
section {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: calc(100dvh - 7rem + 3px);
color: white;
text-align: center;
padding: 2rem;
@media (max-width: 768px) {
padding: 1rem;
}
max-width: 1200px;
margin: 0 auto;
}
section h1 {
font-size: 6rem;
margin: 0;
font-weight: 900;
}
section p {
font-size: 1.5rem;
margin: 1rem 0 2rem 0;
opacity: 0.8;
}
section a {
color: white;
text-decoration: none;
padding: 0.75rem 1.5rem;
border: 1px solid white;
transition: background 0.2s;
}
section a:hover {
background: rgba(255, 255, 255, 0.1);
}
</style>
================================================
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";
---
<DefaultLayout title={title} description={description} ogImage={ogImage}>
<Fragment slot="main">
<section>
<p>Hi, I am Matthias.</p>
<p>
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.
</p>
<p>
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 <a href="/resume">resume</a>, or follow along a
bit more ephemerally by checking out what I'm up to <a
href="/now">now</a
>.
</p>
<p>
Currently based in Southern California, where I spend a lot of
time cooking or behind the lens when I'm not at my desk.
</p>
</section>
</Fragment>
</DefaultLayout>
<style>
section {
padding: 2rem;
@media (max-width: 768px) {
padding: 1rem;
}
max-width: 1200px;
width: 100%;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 1rem;
height: fit-content;
min-height: calc(50vh - 5rem);
}
section > * {
max-width: 64ch;
word-wrap: pretty;
}
#social ul {
list-style: none;
padding: 0;
margin: 0;
}
#social li {
border-bottom: 1px solid var(--color-border);
padding-bottom: 1rem;
}
#social li:last-child {
border-bottom: none;
}
#social li a {
text-decoration: none;
color: inherit;
font-weight: 700;
}
#social li a:hover {
text-decoration: underline;
}
.username {
font-weight: 400;
color: var(--color-muted);
}
</style>
================================================
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 || "")}`;
---
<DefaultLayout title={title} description={description} ogImage={ogImage}>
<Fragment slot='main'>
<article>
<nav class='breadcrumb'>
<a href='/content'>All Content</a> / <a href={`/content/${collection}`}>{collection}</a> / {slug}
</nav>
<header>
<h1>{entry.data.title}</h1>
<div class='meta'>
<time datetime={entry.data.created}>
Created: {new Date(entry.data.created).toLocaleDateString()}
</time>
<time datetime={entry.data.updated}>
Updated: {new Date(entry.data.updated).toLocaleDateString()}
</time>
</div>
{
entry.data.tags && entry.data.tags.length > 0 && (
<div class='tags'>
{entry.data.tags.map((tag: string) => (
<a href={`/tags/${tag}`} class='tag'>
{tag}
</a>
))}
</div>
)
}
{entry.data.excerpt && <p class='excerpt'>{entry.data.excerpt}</p>}
</header>
<div class='content'>
<Content />
</div>
</article>
</Fragment>
<!-- <Sidebar slot='sidebar'>
<h2>sidebar</h2>
</Sidebar> -->
</DefaultLayout>
<style>
article {
padding: 1rem;
max-width: 800px;
margin: 0 auto 5rem;
}
.breadcrumb {
font-size: 0.8rem;
color: var(--color-muted);
margin-bottom: 2rem;
}
.breadcrumb a {
color: inherit;
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
header {
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid var(--color-border);
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
font-size: 0.8rem;
color: var(--color-muted);
margin-bottom: 1rem;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tag {
background: var(--color-surface);
padding: 0.25rem 0.75rem;
text-decoration: none;
color: inherit;
}
.tag:hover {
background: var(--color-surface);
opacity: 0.8;
}
.excerpt {
font-style: italic;
line-height: 1.6;
margin: 0;
font-size: 0.9rem;
}
.content {
max-width: 100%;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
</style>
================================================
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<string, Entry[]>();
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}`;
---
<DefaultLayout title={title} description={description} ogImage={ogImage}>
<Fragment slot='main'>
<section>
<nav class='breadcrumb'>
<a href='/content'>All Content</a> / {collection}
</nav>
<h1>{collection}</h1>
<div class='header-meta'>
<p class='count'>{entries.length} items</p>
<a href={`/content/${collection}/rss.xml`} class='rss-link'>RSS</a>
</div>
<div class='entries-list'>
{
entries.map((entry) => (
<article class='entry-card'>
<h2>
<a href={`/content/${collection}/${entry.id}`}>{entry.data.title}</a>
</h2>
{entry.data.excerpt && <p class='excerpt'>{entry.data.excerpt}</p>}
<div class='meta'>
<time datetime={entry.data.created}>Created: {new Date(entry.data.created).toLocaleDateString()}</time>
{entry.data.tags && entry.data.tags.length > 0 && (
<div class='tags'>
{entry.data.tags.map((tag: string) => (
<a href={`/tags/${tag}`} class='tag'>
{tag}
</a>
))}
</div>
)}
</div>
</article>
))
}
</div>
{
byTag.size > 0 && (
<aside class='tags-sidebar'>
<h3>Filter by tag</h3>
<div class='tag-cloud'>
{Array.from(byTag.entries())
.sort((a, b) => b[1].length - a[1].length)
.map(([tag, items]) => (
<button class='tag-filter' data-tag={tag}>
{tag} <span class='tag-count'>({items.length})</span>
</button>
))}
</div>
</aside>
)
}
</section>
</Fragment>
</DefaultLayout>
<style>
section {
padding: 2rem;
@media (max-width: 768px) {
padding: 1rem;
}
max-width: 1200px;
margin: 0 auto;
}
.breadcrumb {
font-size: 0.9rem;
color: var(--color-muted);
margin-bottom: 1rem;
}
.breadcrumb a {
color: inherit;
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
h1 {
margin-bottom: 0.5rem;
text-transform: capitalize;
}
.header-meta {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
}
.count {
color: var(--color-muted);
margin: 0;
}
.rss-link {
font-size: 0.85rem;
text-decoration: none;
color: inherit;
border: 1px solid var(--color-border);
padding: 0.25rem 0.5rem;
}
.rss-link:hover {
background: var(--color-surface);
}
.entries-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.entry-card {
border: 1px solid var(--color-border);
padding: 1.5rem;
@media (max-width: 768px) {
padding: 1rem;
}
}
.entry-card h2 {
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
}
.entry-card h2 a {
text-decoration: none;
color: inherit;
}
.entry-card h2 a:hover {
text-decoration: underline;
}
.excerpt {
margin: 0 0 1rem 0;
line-height: 1.6;
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
font-size: 0.9rem;
color: var(--color-muted);
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag {
background: var(--color-surface);
padding: 0.25rem 0.5rem;
text-decoration: none;
color: inherit;
}
.tag:hover {
background: var(--color-surface);
opacity: 0.8;
}
.tags-sidebar {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--color-border);
}
.tags-sidebar h3 {
margin: 0 0 1rem 0;
}
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag-filter {
background: var(--color-surface);
border: 1px solid var(--color-border);
padding: 0.5rem 1rem;
color: var(--color-foreground);
cursor: pointer;
font-size: 0.9rem;
}
.tag-filter:hover {
background: var(--color-surface);
opacity: 0.8;
}
.tag-count {
color: var(--color-muted);
}
</style>
================================================
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: '<language>en-us</language>',
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";
---
<DefaultLayout title={title} description={description} ogImage={ogImage}>
<Fragment slot='main'>
<section>
<h1>All Content</h1>
<div class='header-meta'>
<p class='count'>{allEntriesWithCollection.length} items</p>
<a href='/rss.xml' class='rss-link'>RSS</a>
</div>
<div class='entries-list'>
{
allEntriesWithCollection.map((entry) => (
<article class='entry-card'>
<h2>
<a href={`/content/${entry.collection}/${entry.id}`}>{entry.data.title}</a>
</h2>
{entry.data.excerpt && <p class='excerpt'>{entry.data.excerpt}</p>}
<div class='meta'>
<time datetime={entry.data.created}>Created: {new Date(entry.data.created).toLocaleDateString()}</time>
<a href={`/content/${entry.collection}`} class='collection-link'>
{entry.collection}
</a>
{entry.data.tags && entry.data.tags.length > 0 && (
<div class='tags'>
{entry.data.tags.map((tag: string) => (
<a href={`/tags/${tag}`} class='tag'>
{tag}
</a>
))}
</div>
)}
</div>
</article>
))
}
</div>
</section>
</Fragment>
</DefaultLayout>
<style>
section {
padding: 2rem;
@media (max-width: 768px) {
padding: 1rem;
}
max-width: 1200px;
margin: 0 auto;
}
h1 {
margin-bottom: 0.5rem;
}
.header-meta {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
}
.count {
color: var(--color-muted);
margin: 0;
}
.rss-link {
font-size: 0.85rem;
text-decoration: none;
color: inherit;
border: 1px solid var(--color-border);
padding: 0.25rem 0.5rem;
}
.rss-link:hover {
background: var(--color-surface);
}
.entries-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.entry-card {
border: 1px solid var(--color-border);
padding: 1.5rem;
@media (max-width: 768px) {
padding: 1rem;
}
}
.entry-card h2 {
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
}
.entry-card h2 a {
text-decoration: none;
color: inherit;
}
.entry-card h2 a:hover {
text-decoration: underline;
}
.excerpt {
margin: 0 0 1rem 0;
line-height: 1.6;
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
font-size: 0.9rem;
color: var(--color-muted);
}
.collection-link {
text-decoration: none;
color: inherit;
text-transform: capitalize;
padding: 0.25rem 0.5rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
}
.collection-link:hover {
opacity: 0.8;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag {
background: var(--color-surface);
padding: 0.25rem 0.5rem;
text-decoration: none;
color: inherit;
}
.tag:hover {
background: var(--color-surface);
opacity: 0.8;
}
</style>
================================================
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 = "/";
---
<DefaultLayout title={title} description={description} ogImage={ogImage}>
<Fragment slot='main'>
<HeroSection />
<RecentContent />
<RecentGlass client:load />
<!-- <RecentFeeds /> -->
</Fragment>
</DefaultLayout>
================================================
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.",
},
];
---
<DefaultLayout title={title} description={description} ogImage={ogImage}>
<Fragment slot="main">
<section>
<h1>Now</h1>
<p>A short list of things I'm working on:</p>
<ul>
<li>DevRel @ pinata</li>
<li>Building <a href="https://llm-txt.fun">LLM TXT</a></li>
<li>
Cooking - check out my collection of <a href="/recipes"
>recipes</a
>
</li>
</ul>
<p class="muted">Last updated April 1st, 2026</p>
</section>
<section>
<h3>Updates</h3>
<SocialFeeds client:load limit={5} />
</section>
<section>
<h2>Find me online</h2>
<ul>
{
socialLinks.map((link) => (
<li>
<>
<a href={link.url}>
{link.name}{" "}
<span class="username">
{link.username}
</span>
</a>
<br />
</>
<!-- <small>{link.blurb}</small> -->
</li>
))
}
</ul>
</section>
</Fragment>
</DefaultLayout>
<style>
section {
padding: 2rem;
@media (max-width: 768px) {
padding: 1rem;
}
max-width: 1200px;
width: 100%;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 1rem;
height: fit-content;
}
h1 {
margin: 0;
font-size: 1.5rem;
}
</style>
================================================
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`;
---
<DefaultLayout title={title} description={description} ogImage={ogImage}>
<Fragment slot='main'>
<article>
<a href='/onchain-analytics' class='back'>Back</a>
<svg
viewBox='-10 -10 140 160'
preserveAspectRatio='xMidYMid meet'
xmlns='http://www.w3.org/2000/svg'
style='display: block;'
>
<defs>
<clipPath id='clip-bg'>
<rect x='-10' y='-10' width='140' height='160'></rect>
</clipPath>
</defs>
<!-- Background -->
<rect x='-10' y='-10' width='140' height='160' fill={colors[0]} opacity='0.15'></rect>
<g clip-path='url(#clip-bg)'>
<!-- Background Pattern -->
{patterns.background.map((path) => <path d={path} fill={colors[1]} opacity='0.15' />)}
<!-- Primary Regions with Patterns -->
{
patterns.patterns.map((pattern, i) => (
<>
{pattern.type === "cells"
? pattern.elements.map((path) => (
<path d={path} fill={colors[1 + (i % (colors.length - 1))]} opacity='0.95' />
))
: pattern.elements.map((path) => (
<path
d={path}
fill='none'
stroke={colors[1 + (i % (colors.length - 1))]}
stroke-width='0.4'
opacity='0.9'
/>
))}
</>
))
}
</g>
</svg>
<p>{hash}</p>
</article>
</Fragment>
</DefaultLayout>
<style>
article {
padding: 1rem;
max-width: 600px;
margin: 0 auto 5rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
width: 100%;
text-align: center;
}
svg {
width: 100%;
height: auto;
margin: 0 auto;
display: block;
aspect-ratio: 7 / 8;
max-height: 70vh;
}
p {
word-break: break-all;
font-family: monospace;
font-size: 0.8rem;
}
</style>
================================================
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";
---
<DefaultLayout title={title} description={description} ogImage={ogImage}>
<Fragment slot="main">
<article>
<nav class="breadcrumb">
<a href="/content">All Content</a> / Onchain Analytics
</nav>
<div class="content">
<h1>Onchain Analytics</h1>
<p>
Recording started at block number <a
href="https://sepolia.basescan.org/tx/0xc6fffb63218605b36b5cb7b07df1e66be77015e4ede9fe03b0356dd18d1fb8f8"
>12315477
</a>, and is no longer collecting data. The <a
href=`https://sepolia.basescan.org/address/${CONTRACT_ADDRESS}#code#L1`
>
smart contracts
</a> are verified on Basescan.
</p>
<p>
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 <a
class="break"
href="https://iammatthias.com/content/posts/1712329304675-onchain-hit-counter/"
>read more it here</a
>.
</p>
<OnchainAnalytics
client:only="react"
contractAddress={CONTRACT_ADDRESS}
/>
</div>
</article>
</Fragment>
<!-- <Sidebar slot='sidebar'>
<h2>sidebar</h2>
</Sidebar> -->
</DefaultLayout>
<style>
article {
padding: 2rem;
@media (max-width: 768px) {
padding: 1rem;
}
@media (max-width: 768px) {
padding: 1rem;
}
max-width: 1200px;
margin: 0 auto;
max-width: 800px;
margin: 0 auto 5rem;
}
.breadcrumb {
font-size: 0.8rem;
color: var(--color-muted);
margin-bottom: 2rem;
}
.breadcrumb a {
color: inherit;
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
header {
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid var(--color-border);
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
font-size: 0.8rem;
color: var(--color-muted);
margin-bottom: 1rem;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tag {
background: var(--color-surface);
padding: 0.25rem 0.75rem;
text-decoration: none;
color: inherit;
}
.tag:hover {
background: var(--color-surface);
opacity: 0.8;
}
.excerpt {
font-style: italic;
line-height: 1.6;
margin: 0;
font-size: 0.9rem;
}
.content {
max-width: 100%;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
</style>
================================================
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";
---
<DefaultLayout title={title} description={description} ogImage={ogImage}>
<Fragment slot='main'>
{
resumeData.map((section) => (
<section>
<h1>{section.section}</h1>
{section.entries.map((entry) => (
<div>
<h2>{entry.company}</h2>
<p>
<strong>{entry.position}</strong>
</p>
<p>{entry.date}</p>
</div>
))}
</section>
))
}
</Fragment>
</DefaultLayout>
<style>
section {
padding: 2rem;
@media (max-width: 768px) {
padding: 1rem;
}
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 1rem;
height: fit-content;
min-height: calc(50vh - 5rem);
}
section > * {
max-width: 64ch;
word-wrap: pretty;
}
#social li {
border-bottom: 1px solid var(--color-border);
padding-bottom: 1rem;
}
#social li:last-child {
border-bottom: none;
}
#social li a {
text-decoration: none;
color: inherit;
font-weight: 700;
}
#social li a:hover {
text-decoration: underline;
}
.username {
font-weight: 400;
color: var(--color-muted);
}
</style>
================================================
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: "<language>en-us</language>",
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`;
---
<DefaultLayout title={title} description={description} ogImage={ogImage}>
<Fragment slot='main'>
<section>
<nav class='breadcrumb'>
<a href='/tags'>All Tags</a> / {tagName}
</nav>
<h1>#{tagName}</h1>
<div class='header-meta'>
<p class='count'>{entriesWithTag.length} {entriesWithTag.length === 1 ? "entry" : "entries"}</p>
</div>
<div class='entries-list'>
{
entriesWithTag.map((entry) => (
<article class='entry-card'>
<h2>
<a href={`/content/${entry.collection}/${entry.id}`}>{entry.data.title}</a>
</h2>
<p class='collection'>{entry.collection}</p>
{entry.data.excerpt && <p class='excerpt'>{entry.data.excerpt}</p>}
<div class='meta'>
<time datetime={entry.data.created}>{new Date(entry.data.created).toLocaleDateString()}</time>
</div>
</article>
))
}
</div>
</section>
</Fragment>
</DefaultLayout>
<style>
section {
padding: 2rem;
@media (max-width: 768px) {
padding: 1rem;
}
max-width: 1200px;
margin: 0 auto;
}
.breadcrumb {
font-size: 0.9rem;
color: var(--color-muted);
margin-bottom: 1rem;
}
.breadcrumb a {
color: inherit;
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
h1 {
margin-bottom: 0.5rem;
}
.header-meta {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
}
.count {
color: var(--color-muted);
margin: 0;
}
.rss-link {
font-size: 0.85rem;
text-decoration: none;
color: inherit;
border: 1px solid var(--color-border);
padding: 0.25rem 0.5rem;
}
.rss-link:hover {
background: var(--color-surface);
}
.entries-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.entry-card {
border: 1px solid var(--color-border);
padding: 1.5rem;
@media (max-width: 768px) {
padding: 1rem;
}
}
.entry-card h2 {
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
}
.entry-card h2 a {
text-decoration: none;
color: inherit;
}
.entry-card h2 a:hover {
text-decoration: underline;
}
.collection {
color: var(--color-muted);
margin: 0 0 0.5rem 0;
text-transform: capitalize;
}
.excerpt {
color: var(--color-invisibles);
margin: 0 0 1rem 0;
line-height: 1.6;
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
font-size: 0.9rem;
color: var(--color-muted);
}
</style>
================================================
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";
---
<DefaultLayout title={title} description={description} ogImage={ogImage}>
<Fragment slot='main'>
<section>
<h1>All Tags</h1>
<p>{tags.length} tags</p>
<div class='tags-grid'>
{
tags.map((tag) => (
<a href={`/tags/${tag.id}`} class='tag-card'>
<h2>{tag.data.name}</h2>
<p class='count'>
{tag.data.count} {tag.data.count === 1 ? "collection" : "collections"}
</p>
</a>
))
}
</div>
</section>
</Fragment>
</DefaultLayout>
<style>
section {
padding: 2rem;
@media (max-width: 768px) {
padding: 1rem;
}
max-width: 1200px;
width: 100%;
margin: 0 auto;
}
h1 {
margin-bottom: 0.5rem;
}
.tags-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
margin-top: 2rem;
}
.tag-card {
border: 1px solid var(--color-border);
padding: 1rem;
text-decoration: none;
color: inherit;
}
.tag-card:hover {
background: var(--color-surface);
}
.tag-card h2 {
margin: 0 0 0.5rem 0;
font-size: 1.25rem;
}
.count {
color: var(--color-muted);
font-size: 0.9rem;
margin: 0;
}
</style>
================================================
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
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
SYMBOL INDEX (115 symbols across 16 files)
FILE: astro.config.mjs
function rawFonts (line 17) | function rawFonts(extensions) {
FILE: src/components/BlackHole/index.tsx
class SeededRNG (line 7) | class SeededRNG {
method constructor (line 10) | constructor(seed: number) {
method next (line 15) | next() {
function generateStarTexture (line 22) | function generateStarTexture(seed: number, size: number = 1024): THREE.D...
function BlackHoleShader (line 71) | function BlackHoleShader({ seed, iterations }: { seed: number; iteration...
function BlackHole (line 133) | function BlackHole({ seed = 404 }: { seed?: number }) {
FILE: src/components/Bluesky/index.tsx
type NormalizedImage (line 5) | interface NormalizedImage {
type NormalizedVideo (line 14) | interface NormalizedVideo {
type NormalizedExternal (line 22) | interface NormalizedExternal {
type NormalizedQuote (line 30) | interface NormalizedQuote {
type NormalizedEmbed (line 36) | type NormalizedEmbed =
type BlueskyPost (line 42) | interface BlueskyPost {
function formatDate (line 51) | function formatDate(dateString: string): string {
function ImageEmbed (line 71) | function ImageEmbed({ embed }: { embed: NormalizedImage }) {
function VideoEmbed (line 84) | function VideoEmbed({ embed }: { embed: NormalizedVideo }) {
function ExternalEmbed (line 96) | function ExternalEmbed({ embed }: { embed: NormalizedExternal }) {
function QuoteEmbed (line 121) | function QuoteEmbed({ embed }: { embed: NormalizedQuote }) {
function EmbedRenderer (line 139) | function EmbedRenderer({ embed }: { embed: NormalizedEmbed }) {
function PostCard (line 154) | function PostCard({ post }: { post: BlueskyPost }) {
type BlueskyFeedProps (line 192) | interface BlueskyFeedProps {
function BlueskyFeed (line 196) | function BlueskyFeed({ limit = 10 }: BlueskyFeedProps) {
FILE: src/components/Farcaster/index.tsx
type NormalizedEmbed (line 4) | interface NormalizedEmbed {
type FarcasterPost (line 11) | interface FarcasterPost {
function formatDate (line 19) | function formatDate(dateString: string): string {
function isImageUrl (line 39) | function isImageUrl(url: string): boolean {
function UrlEmbed (line 43) | function UrlEmbed({ url }: { url: string }) {
function PostCard (line 59) | function PostCard({ post }: { post: FarcasterPost }) {
type FarcasterFeedProps (line 104) | interface FarcasterFeedProps {
function FarcasterFeed (line 109) | function FarcasterFeed({ limit = 10, onDataLoaded }: FarcasterFeedProps) {
FILE: src/components/OnchainAnalytics/index.tsx
type PageView (line 45) | interface PageView {
type Event (line 50) | interface Event {
type OnchainAnalyticsProps (line 55) | interface OnchainAnalyticsProps {
function OnchainAnalytics (line 59) | function OnchainAnalytics({ contractAddress }: OnchainAnalyticsProps) {
FILE: src/components/RecentGlass/index.tsx
type GlassExif (line 5) | interface GlassExif {
type GlassPhoto (line 14) | interface GlassPhoto {
function RecentGlass (line 25) | function RecentGlass() {
FILE: src/components/SocialFeeds/index.tsx
type BasePost (line 5) | interface BasePost {
type TextPost (line 10) | interface TextPost extends BasePost {
type BlueskyImage (line 15) | interface BlueskyImage {
type BlueskyVideo (line 23) | interface BlueskyVideo {
type BlueskyExternal (line 29) | interface BlueskyExternal {
type BlueskyQuote (line 37) | interface BlueskyQuote {
type BlueskyEmbed (line 43) | type BlueskyEmbed =
type BlueskyPost (line 49) | interface BlueskyPost extends TextPost {
type FarcasterEmbed (line 56) | interface FarcasterEmbed {
type FarcasterPost (line 63) | interface FarcasterPost extends TextPost {
type GlassExif (line 69) | interface GlassExif {
type GlassPost (line 78) | interface GlassPost extends BasePost {
constant STALE_THRESHOLD_MS (line 89) | const STALE_THRESHOLD_MS = 5 * 24 * 60 * 60 * 1000;
function isPostStale (line 91) | function isPostStale(post: BasePost): boolean {
function filterFreshPosts (line 96) | function filterFreshPosts<T extends BasePost>(posts: T[]): T[] {
function formatDate (line 100) | function formatDate(dateString: string): string {
function isImageUrl (line 120) | function isImageUrl(url: string): boolean {
function BlueskyPostCard (line 131) | function BlueskyPostCard({ post }: { post: BlueskyPost }) {
function FarcasterPostCard (line 228) | function FarcasterPostCard({ post }: { post: FarcasterPost }) {
function GlassPostCard (line 305) | function GlassPostCard({ post }: { post: GlassPost }) {
type SocialFeedsProps (line 358) | interface SocialFeedsProps {
function SocialFeeds (line 362) | function SocialFeeds({ limit = 10 }: SocialFeedsProps) {
FILE: src/content.config.ts
constant GITHUB_OWNER (line 6) | const GITHUB_OWNER = 'iammatthias';
constant GITHUB_REPO (line 7) | const GITHUB_REPO = 'obsidian_cms';
constant GITHUB_BRANCH (line 8) | const GITHUB_BRANCH = 'main';
constant GITHUB_TOKEN (line 9) | const GITHUB_TOKEN = import.meta.env.GITHUB;
FILE: src/lib/github-loader.ts
type GitHubLoaderOptions (line 4) | interface GitHubLoaderOptions {
type GitHubContentData (line 22) | type GitHubContentData = z.infer<typeof githubContentSchema>;
type CachedFile (line 28) | interface CachedFile {
type CachedCollection (line 35) | interface CachedCollection {
type ContentCache (line 40) | interface ContentCache {
function populateCache (line 59) | async function populateCache(
function getCachedCollection (line 182) | function getCachedCollection(
function getCachedTagMap (line 191) | function getCachedTagMap(): Map<string, Set<string>> {
function ensureCachePopulated (line 198) | async function ensureCachePopulated(
function githubLoader (line 212) | function githubLoader(options: GitHubLoaderOptions): Loader {
function parseFrontmatter (line 337) | function parseFrontmatter(content: string): {
function parseYaml (line 357) | function parseYaml(yaml: string): Record<string, any> {
function getGitHubCollections (line 423) | async function getGitHubCollections(
FILE: src/lib/og-renderer.ts
function renderToPng (line 8) | async function renderToPng(
FILE: src/lib/tags-loader.ts
type TagsLoaderOptions (line 5) | interface TagsLoaderOptions {
type TagData (line 19) | type TagData = z.infer<typeof tagsSchema>;
function tagsLoader (line 21) | function tagsLoader(options: TagsLoaderOptions): Loader {
FILE: src/pages/api/bluesky.json.ts
constant USER_DID (line 4) | const USER_DID = "did:plc:p5xem22ammiafn5kxonaksfa";
constant PDS_HOST (line 5) | const PDS_HOST = "https://pds.iammatthias.com";
type BlobRef (line 8) | interface BlobRef {
type RawImageEmbed (line 15) | interface RawImageEmbed {
type RawVideoEmbed (line 24) | interface RawVideoEmbed {
type RawExternalEmbed (line 31) | interface RawExternalEmbed {
type RawRecordEmbed (line 41) | interface RawRecordEmbed {
type RawRecordWithMediaEmbed (line 49) | interface RawRecordWithMediaEmbed {
type RawEmbed (line 55) | type RawEmbed =
type RawPostRecord (line 62) | interface RawPostRecord {
type PDSRecord (line 83) | interface PDSRecord {
type ListRecordsResponse (line 89) | interface ListRecordsResponse {
type NormalizedImage (line 95) | interface NormalizedImage {
type NormalizedVideo (line 104) | interface NormalizedVideo {
type NormalizedExternal (line 112) | interface NormalizedExternal {
type NormalizedQuote (line 120) | interface NormalizedQuote {
type NormalizedEmbed (line 126) | type NormalizedEmbed =
function getBlobUrl (line 133) | function getBlobUrl(did: string, cid: string): string {
function normalizeEmbed (line 137) | function normalizeEmbed(embed: RawEmbed, did: string): NormalizedEmbed[] {
FILE: src/pages/api/farcaster.json.ts
constant HUB_URL (line 4) | const HUB_URL = "https://hub.merv.fun";
constant USER_FID (line 5) | const USER_FID = 2728;
constant FARCASTER_EPOCH (line 8) | const FARCASTER_EPOCH = 1609459200;
type FarcasterEmbed (line 10) | interface FarcasterEmbed {
type FarcasterCastAddBody (line 18) | interface FarcasterCastAddBody {
type FarcasterMessage (line 30) | interface FarcasterMessage {
type FarcasterResponse (line 40) | interface FarcasterResponse {
type NormalizedEmbed (line 46) | interface NormalizedEmbed {
function farcasterTimestampToDate (line 53) | function farcasterTimestampToDate(timestamp: number): Date {
FILE: src/pages/api/glass.json.ts
type GlassExif (line 5) | interface GlassExif {
type GlassPhoto (line 14) | interface GlassPhoto {
FILE: src/pages/content/[collection]/rss.xml.ts
type Entry (line 6) | type Entry = {
function getStaticPaths (line 11) | async function getStaticPaths() {
function GET (line 29) | async function GET(context: APIContext) {
FILE: src/pages/rss.xml.ts
type Entry (line 6) | type Entry = {
function GET (line 12) | async function GET(context: APIContext) {
Condensed preview — 56 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (190K chars).
[
{
"path": ".cursor/mcp.json",
"chars": 122,
"preview": "{\n \"mcpServers\": {\n \"Astro docs\": {\n \"type\": \"http\",\n \"url\": \"https://mcp.docs.astro.build/mcp\"\n }\n }\n"
},
{
"path": ".gitignore",
"chars": 344,
"preview": "# build output\ndist/\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyarn"
},
{
"path": ".gitmodules",
"chars": 87,
"preview": "[submodule \"worker-og\"]\n\tpath = worker-og\n\turl = https://github.com/iammatthias/og.git\n"
},
{
"path": ".vscode/extensions.json",
"chars": 87,
"preview": "{\n \"recommendations\": [\"astro-build.astro-vscode\"],\n \"unwantedRecommendations\": []\n}\n"
},
{
"path": ".vscode/launch.json",
"chars": 207,
"preview": "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\n \"command\": \"./node_modules/.bin/astro dev\",\n \"name\": \"Dev"
},
{
"path": "README.md",
"chars": 1375,
"preview": "```\n\n`7MMF'\n MM\n MM ,6\"Yb. `7MMpMMMb.pMMMb.\n MM 8) MM MM MM MM\n MM ,pm9MM MM MM "
},
{
"path": "astro.config.mjs",
"chars": 3293,
"preview": "// @ts-check\nimport { defineConfig } from \"astro/config\";\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimpor"
},
{
"path": "package.json",
"chars": 969,
"preview": "{\n \"name\": \"com\",\n \"type\": \"module\",\n \"version\": \"0.0.1\",\n \"scripts\": {\n \"dev\": \"astro dev\",\n \"build\": \"astro "
},
{
"path": "public/.well-known/atproto-did",
"chars": 33,
"preview": "did:plc:p5xem22ammiafn5kxonaksfa\n"
},
{
"path": "public/rss.xml.xsl",
"chars": 6806,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n "
},
{
"path": "src/components/BlackHole/index.tsx",
"chars": 4829,
"preview": "import { useRef, useMemo, useEffect } from \"react\";\nimport { Canvas, useFrame, useThree } from \"@react-three/fiber\";\nimp"
},
{
"path": "src/components/BlackHole/shaders.ts",
"chars": 4155,
"preview": "export const vertexShader = `\n varying vec2 vUv;\n\n void main() {\n gl_Position = projectionMatrix * modelViewMatrix "
},
{
"path": "src/components/Bluesky/Bluesky.css",
"chars": 4934,
"preview": ".bsky-container {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.bsky-feed {\n display: flex;\n f"
},
{
"path": "src/components/Bluesky/index.tsx",
"chars": 6878,
"preview": "import { useEffect, useState } from \"react\";\nimport \"./Bluesky.css\";\n\n// Types matching the API response\ninterface Norma"
},
{
"path": "src/components/Farcaster/Farcaster.css",
"chars": 2508,
"preview": ".fc-container {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.fc-feed {\n display: flex;\n flex-direction:"
},
{
"path": "src/components/Farcaster/index.tsx",
"chars": 4916,
"preview": "import { useEffect, useState } from \"react\";\nimport \"./Farcaster.css\";\n\ninterface NormalizedEmbed {\n type: \"url\" | \"cas"
},
{
"path": "src/components/OnchainAnalytics/OnchainAnalytics.css",
"chars": 777,
"preview": ".onchain-analytics-container {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.onchain-analytics-container >"
},
{
"path": "src/components/OnchainAnalytics/index.tsx",
"chars": 6781,
"preview": "import { useEffect, useState } from \"react\";\nimport { baseSepoliaClient } from \"@lib/viemProvider\";\nimport \"./OnchainAna"
},
{
"path": "src/components/RecentGlass/RecentGlass.css",
"chars": 1498,
"preview": ".recent-glass-container {\n margin-top: auto;\n min-height: calc(50vh - 2rem);\n border-top: 1px solid var(--color-borde"
},
{
"path": "src/components/RecentGlass/index.tsx",
"chars": 3148,
"preview": "import { useEffect, useState } from \"react\";\nimport { Masonry } from \"react-plock\";\nimport \"./RecentGlass.css\";\n\ninterfa"
},
{
"path": "src/components/SocialFeeds/SocialFeeds.css",
"chars": 4638,
"preview": ".social-feeds {\n display: grid;\n gap: 2rem;\n width: 100%;\n}\n\n.social-feeds-three {\n grid-template-columns: 1"
},
{
"path": "src/components/SocialFeeds/index.tsx",
"chars": 14699,
"preview": "import { useEffect, useState } from \"react\";\nimport \"./SocialFeeds.css\";\n\n// Shared types\ninterface BasePost {\n created"
},
{
"path": "src/components/footer.astro",
"chars": 657,
"preview": "---\n\n---\n\n<footer>\n <p><a href=\"/now\">Now</a></p>\n <p><a href=\"/tags\">Tags</a></p>\n <p><a href=\"/rss.xml\">RSS</"
},
{
"path": "src/components/homepage/heroSection.astro",
"chars": 1131,
"preview": "---\n\n---\n\n<section>\n <h1>Hi, I am Matthias</h1>\n <p>\n I run DevRel at <a href='https://www.pinata.cloud/' title='Pi"
},
{
"path": "src/components/homepage/recentContent.astro",
"chars": 3947,
"preview": "---\nimport { getCollection } from \"astro:content\";\nimport { getGitHubCollections, type GitHubContentData } from \"@lib/gi"
},
{
"path": "src/components/homepage/recentFeeds.astro",
"chars": 743,
"preview": "---\n\n---\n\n<section>\n <h2>feeds</h2>\n <p>blogs I like that have rss</p>\n <div class='grid'>\n <ul><li>item 1</li><li"
},
{
"path": "src/components/nav.astro",
"chars": 1251,
"preview": "---\n\n---\n\n<nav>\n <p><a href=\"/\">Home</a></p>\n <p><a href=\"/content\">Content</a></p>\n <p><a href=\"/about\">About<"
},
{
"path": "src/components/sidebar/index.astro",
"chars": 4929,
"preview": "---\n\n---\n\n<details id='sidebar' class='sidebar' open>\n <summary class='sidebar-toggle' aria-label='Toggle sidebar'>\n "
},
{
"path": "src/content.config.ts",
"chars": 1150,
"preview": "import { defineCollection } from 'astro:content';\nimport { githubLoader, getGitHubCollections } from './lib/github-loade"
},
{
"path": "src/layouts/defaultLayouts.astro",
"chars": 3428,
"preview": "---\nimport \"@src/styles/reset.css\";\nimport \"@src/styles/globals.css\";\nimport Nav from \"@components/nav.astro\";\nimport Fo"
},
{
"path": "src/lib/github-loader.ts",
"chars": 11081,
"preview": "import type { Loader, LoaderContext } from \"astro/loaders\";\nimport { z } from \"astro/zod\";\n\ninterface GitHubLoaderOption"
},
{
"path": "src/lib/og-renderer.ts",
"chars": 698,
"preview": "import satori from \"satori\";\nimport { html } from \"satori-html\";\nimport { Resvg } from \"@cf-wasm/resvg\";\n\n/**\n * Renders"
},
{
"path": "src/lib/tags-loader.ts",
"chars": 1946,
"preview": "import type { Loader, LoaderContext } from \"astro/loaders\";\nimport { z } from \"astro/zod\";\nimport { ensureCachePopulated"
},
{
"path": "src/lib/viemProvider.ts",
"chars": 194,
"preview": "import { createPublicClient, http } from 'viem';\nimport { baseSepolia } from 'viem/chains';\n\nexport const baseSepoliaCli"
},
{
"path": "src/pages/404.astro",
"chars": 1785,
"preview": "---\nexport const prerender = false;\nimport DefaultLayout from \"@layouts/defaultLayouts.astro\";\nimport BlackHole from \"@s"
},
{
"path": "src/pages/about.astro",
"chars": 2235,
"preview": "---\nimport DefaultLayout from \"@layouts/defaultLayouts.astro\";\n\nconst title = \"About - @iammatthias\";\nconst description "
},
{
"path": "src/pages/api/bluesky.json.ts",
"chars": 6816,
"preview": "import type { APIRoute } from \"astro\";\n\n// PDS configuration\nconst USER_DID = \"did:plc:p5xem22ammiafn5kxonaksfa\";\nconst "
},
{
"path": "src/pages/api/farcaster.json.ts",
"chars": 4000,
"preview": "import type { APIRoute } from \"astro\";\n\n// Farcaster Hub configuration\nconst HUB_URL = \"https://hub.merv.fun\";\nconst USE"
},
{
"path": "src/pages/api/glass.json.ts",
"chars": 3504,
"preview": "import type { APIRoute } from \"astro\";\nimport pLimit from \"p-limit\";\n\n// Glass API types - matches actual API response s"
},
{
"path": "src/pages/content/[collection]/[slug].astro",
"chars": 3638,
"preview": "---\nimport { getCollection, render } from \"astro:content\";\nimport DefaultLayout from \"@layouts/defaultLayouts.astro\";\nim"
},
{
"path": "src/pages/content/[collection]/index.astro",
"chars": 5730,
"preview": "---\nimport { getCollection } from \"astro:content\";\nimport DefaultLayout from \"@layouts/defaultLayouts.astro\";\nimport { g"
},
{
"path": "src/pages/content/[collection]/rss.xml.ts",
"chars": 1551,
"preview": "import rss from '@astrojs/rss';\nimport { getCollection } from 'astro:content';\nimport { getGitHubCollections, type GitHu"
},
{
"path": "src/pages/content/index.astro",
"chars": 4524,
"preview": "---\nimport { getCollection } from \"astro:content\";\nimport DefaultLayout from \"@layouts/defaultLayouts.astro\";\nimport { g"
},
{
"path": "src/pages/index.astro",
"chars": 703,
"preview": "---\nimport DefaultLayout from \"@layouts/defaultLayouts.astro\";\nimport HeroSection from \"@components/homepage/heroSection"
},
{
"path": "src/pages/now.astro",
"chars": 3962,
"preview": "---\nimport DefaultLayout from \"@layouts/defaultLayouts.astro\";\nimport SocialFeeds from \"@components/SocialFeeds/index.ts"
},
{
"path": "src/pages/onchain-analytics/[hash]/index.astro",
"chars": 10335,
"preview": "---\nexport const prerender = false;\nimport DefaultLayout from \"@layouts/defaultLayouts.astro\";\n\nfunction hashToNumbers(h"
},
{
"path": "src/pages/onchain-analytics/index.astro",
"chars": 3541,
"preview": "---\nimport DefaultLayout from \"@layouts/defaultLayouts.astro\";\nimport OnchainAnalytics from \"@components/OnchainAnalytic"
},
{
"path": "src/pages/resume.astro",
"chars": 2750,
"preview": "---\nimport DefaultLayout from \"@layouts/defaultLayouts.astro\";\n\nconst resumeData = [\n {\n section: \"Present\",\n ent"
},
{
"path": "src/pages/robots.txt.ts",
"chars": 294,
"preview": "import type { APIRoute } from \"astro\";\n\nconst getRobotsTxt = (sitemapURL: URL) => `\nUser-agent: *\nAllow: /\n\nSitemap: ${s"
},
{
"path": "src/pages/rss.xml.ts",
"chars": 1655,
"preview": "import rss from \"@astrojs/rss\";\nimport { getCollection } from \"astro:content\";\nimport { getGitHubCollections, type GitHu"
},
{
"path": "src/pages/tags/[tag]/index.astro",
"chars": 4472,
"preview": "---\nimport { getCollection } from \"astro:content\";\nimport DefaultLayout from \"@layouts/defaultLayouts.astro\";\nimport { g"
},
{
"path": "src/pages/tags/index.astro",
"chars": 1754,
"preview": "---\nimport { getCollection } from \"astro:content\";\nimport DefaultLayout from \"@layouts/defaultLayouts.astro\";\nimport typ"
},
{
"path": "src/styles/globals.css",
"chars": 7082,
"preview": ":root {\n /* Previous theme - commented out */\n /* --color-background: #121113;\n --color-foreground: #ffffff;\n --"
},
{
"path": "src/styles/reset.css",
"chars": 1451,
"preview": "/* Box sizing rules */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n text-wrap: pretty"
},
{
"path": "tsconfig.json",
"chars": 530,
"preview": "{\n \"extends\": \"astro/tsconfigs/strict\",\n \"include\": [\".astro/types.d.ts\", \"**/*\"],\n \"exclude\": [\"dist\", \".mastra\"],\n "
},
{
"path": "wrangler.toml",
"chars": 977,
"preview": "name = \"com\"\nmain = \"@astrojs/cloudflare/entrypoints/server\"\ncompatibility_date = \"2024-09-23\"\ncompatibility_flags = [\"n"
}
]
About this extraction
This page contains the full source code of the iammatthias/.com GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 56 files (173.4 KB), approximately 49.6k tokens, and a symbol index with 115 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.