A lexicon-driven AppView for ATProto. happyview.dev
backfill firehose jetstream atproto appview oauth lexicon
8
fork

Configure Feed

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

fix: dynamic page routes were broken

Trezy 791a6a83 7f7105ee

+52 -28
+2
Cargo.toml
··· 20 20 sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "json", "chrono", "migrate"] } 21 21 tokio = { version = "1", features = ["full"] } 22 22 tokio-tungstenite = { version = "0.26", features = ["rustls-tls-webpki-roots"] } 23 + tower = { version = "0.5", features = ["util"] } 23 24 tower-http = { version = "0.6", features = ["cors", "fs", "trace"] } 25 + http-body-util = "0.1" 24 26 hickory-resolver = "0.25" 25 27 mlua = { version = "0.11", features = ["lua54", "async", "serialize", "vendored", "send"] } 26 28 tracing = "0.1"
+42 -3
src/server.rs
··· 1 1 use axum::extract::{DefaultBodyLimit, State}; 2 2 use axum::routing::{get, post}; 3 3 use axum::{Json, Router}; 4 + use bytes::Bytes; 5 + use http_body_util::Full; 6 + use std::convert::Infallible; 4 7 use tower_http::cors::CorsLayer; 5 - use tower_http::services::{ServeDir, ServeFile}; 8 + use tower_http::services::ServeDir; 6 9 use tower_http::trace::TraceLayer; 7 10 8 11 use crate::AppState; ··· 16 19 17 20 pub fn router(state: AppState) -> Router { 18 21 let static_dir = state.config.static_dir.clone(); 19 - let index_path = format!("{}/index.html", static_dir); 20 - let serve_dir = ServeDir::new(&static_dir).not_found_service(ServeFile::new(index_path)); 22 + 23 + // SPA fallback: when ServeDir can't find a static file, check if the 24 + // parent path contains a _/index.html (Next.js dynamic route shell) 25 + // before falling back to the root index.html. 26 + let fallback_dir = static_dir.clone(); 27 + let spa_fallback = tower::service_fn(move |req: axum::http::Request<_>| { 28 + let dir = fallback_dir.clone(); 29 + async move { 30 + let path = req.uri().path(); 31 + let segments: Vec<&str> = path.trim_matches('/').split('/').collect(); 32 + 33 + // Try _/index.html in the parent directory (matches Next.js dynamic routes) 34 + if segments.len() >= 2 { 35 + let parent = segments[..segments.len() - 1].join("/"); 36 + let dynamic_path = format!("{}/{}/_/index.html", dir, parent); 37 + if let Ok(body) = tokio::fs::read(&dynamic_path).await { 38 + return Ok::<_, Infallible>( 39 + axum::http::Response::builder() 40 + .header("content-type", "text/html; charset=utf-8") 41 + .body(Full::new(Bytes::from(body))) 42 + .unwrap(), 43 + ); 44 + } 45 + } 46 + 47 + // Default: serve root index.html 48 + let index = format!("{}/index.html", dir); 49 + let body = tokio::fs::read(&index).await.unwrap_or_default(); 50 + Ok::<_, Infallible>( 51 + axum::http::Response::builder() 52 + .header("content-type", "text/html; charset=utf-8") 53 + .body(Full::new(Bytes::from(body))) 54 + .unwrap(), 55 + ) 56 + } 57 + }); 58 + 59 + let serve_dir = ServeDir::new(&static_dir).not_found_service(spa_fallback); 21 60 22 61 Router::new() 23 62 .route("/health", get(health))
+5 -23
web/package-lock.json
··· 111 111 "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", 112 112 "dev": true, 113 113 "license": "MIT", 114 - "peer": true, 115 114 "dependencies": { 116 115 "@babel/code-frame": "^7.29.0", 117 116 "@babel/generator": "^7.29.0", ··· 739 738 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 740 739 "dev": true, 741 740 "license": "MIT", 742 - "peer": true, 743 741 "engines": { 744 742 "node": ">=12" 745 743 }, ··· 2010 2008 "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", 2011 2009 "dev": true, 2012 2010 "license": "MIT", 2013 - "peer": true, 2014 2011 "engines": { 2015 2012 "node": "^14.21.3 || >=16" 2016 2013 }, ··· 4283 4280 "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", 4284 4281 "dev": true, 4285 4282 "license": "MIT", 4286 - "peer": true, 4287 4283 "dependencies": { 4288 4284 "undici-types": "~7.16.0" 4289 4285 } ··· 4294 4290 "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", 4295 4291 "devOptional": true, 4296 4292 "license": "MIT", 4297 - "peer": true, 4298 4293 "dependencies": { 4299 4294 "csstype": "^3.2.2" 4300 4295 } ··· 4305 4300 "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", 4306 4301 "devOptional": true, 4307 4302 "license": "MIT", 4308 - "peer": true, 4309 4303 "peerDependencies": { 4310 4304 "@types/react": "^19.2.0" 4311 4305 } ··· 4322 4316 "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", 4323 4317 "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", 4324 4318 "license": "MIT", 4325 - "optional": true 4319 + "optional": true, 4320 + "peer": true 4326 4321 }, 4327 4322 "node_modules/@types/unist": { 4328 4323 "version": "3.0.3", ··· 4388 4383 "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", 4389 4384 "dev": true, 4390 4385 "license": "MIT", 4391 - "peer": true, 4392 4386 "dependencies": { 4393 4387 "@typescript-eslint/scope-manager": "8.56.0", 4394 4388 "@typescript-eslint/types": "8.56.0", ··· 4915 4909 "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", 4916 4910 "dev": true, 4917 4911 "license": "MIT", 4918 - "peer": true, 4919 4912 "bin": { 4920 4913 "acorn": "bin/acorn" 4921 4914 }, ··· 5302 5295 "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", 5303 5296 "devOptional": true, 5304 5297 "license": "MIT", 5305 - "peer": true, 5306 5298 "dependencies": { 5307 5299 "@babel/types": "^7.26.0" 5308 5300 } ··· 5397 5389 } 5398 5390 ], 5399 5391 "license": "MIT", 5400 - "peer": true, 5401 5392 "dependencies": { 5402 5393 "baseline-browser-mapping": "^2.9.0", 5403 5394 "caniuse-lite": "^1.0.30001759", ··· 6361 6352 "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", 6362 6353 "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", 6363 6354 "license": "(MPL-2.0 OR Apache-2.0)", 6355 + "peer": true, 6364 6356 "optionalDependencies": { 6365 6357 "@types/trusted-types": "^2.0.7" 6366 6358 } ··· 6699 6691 "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", 6700 6692 "dev": true, 6701 6693 "license": "MIT", 6702 - "peer": true, 6703 6694 "dependencies": { 6704 6695 "@eslint-community/eslint-utils": "^4.8.0", 6705 6696 "@eslint-community/regexpp": "^4.12.1", ··· 6840 6831 "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", 6841 6832 "dev": true, 6842 6833 "license": "MIT", 6843 - "peer": true, 6844 6834 "dependencies": { 6845 6835 "@rtsao/scc": "^1.1.0", 6846 6836 "array-includes": "^3.1.9", ··· 7309 7299 "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", 7310 7300 "dev": true, 7311 7301 "license": "MIT", 7312 - "peer": true, 7313 7302 "dependencies": { 7314 7303 "accepts": "^2.0.0", 7315 7304 "body-parser": "^2.2.1", ··· 8102 8091 "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", 8103 8092 "dev": true, 8104 8093 "license": "MIT", 8105 - "peer": true, 8106 8094 "engines": { 8107 8095 "node": ">=16.9.0" 8108 8096 } ··· 9522 9510 "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", 9523 9511 "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", 9524 9512 "license": "MIT", 9513 + "peer": true, 9525 9514 "bin": { 9526 9515 "marked": "bin/marked.js" 9527 9516 }, ··· 11418 11407 "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", 11419 11408 "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", 11420 11409 "license": "MIT", 11421 - "peer": true, 11422 11410 "engines": { 11423 11411 "node": ">=0.10.0" 11424 11412 } ··· 11449 11437 "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", 11450 11438 "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", 11451 11439 "license": "MIT", 11452 - "peer": true, 11453 11440 "dependencies": { 11454 11441 "scheduler": "^0.27.0" 11455 11442 }, ··· 11469 11456 "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", 11470 11457 "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", 11471 11458 "license": "MIT", 11472 - "peer": true, 11473 11459 "dependencies": { 11474 11460 "@types/use-sync-external-store": "^0.0.6", 11475 11461 "use-sync-external-store": "^1.4.0" ··· 11608 11594 "version": "5.0.1", 11609 11595 "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", 11610 11596 "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", 11611 - "license": "MIT", 11612 - "peer": true 11597 + "license": "MIT" 11613 11598 }, 11614 11599 "node_modules/redux-thunk": { 11615 11600 "version": "3.1.0", ··· 12823 12808 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 12824 12809 "dev": true, 12825 12810 "license": "MIT", 12826 - "peer": true, 12827 12811 "engines": { 12828 12812 "node": ">=12" 12829 12813 }, ··· 13091 13075 "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 13092 13076 "dev": true, 13093 13077 "license": "Apache-2.0", 13094 - "peer": true, 13095 13078 "bin": { 13096 13079 "tsc": "bin/tsc", 13097 13080 "tsserver": "bin/tsserver" ··· 13823 13806 "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", 13824 13807 "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", 13825 13808 "license": "MIT", 13826 - "peer": true, 13827 13809 "funding": { 13828 13810 "url": "https://github.com/sponsors/colinhacks" 13829 13811 }
+3 -2
web/src/app/(dashboard)/lexicons/[id]/lexicon-detail.tsx
··· 1 1 "use client"; 2 2 3 3 import { useCallback, useEffect, useState } from "react"; 4 - import { useParams, useRouter } from "next/navigation"; 4 + import { usePathname, useRouter } from "next/navigation"; 5 5 6 6 import { useAuth } from "@/lib/auth-context"; 7 7 import { CodePanels } from "@/components/code-panels"; ··· 20 20 import { Label } from "@/components/ui/label"; 21 21 22 22 export default function LexiconDetailPage() { 23 - const { id } = useParams<{ id: string }>(); 23 + const pathname = usePathname(); 24 + const id = decodeURIComponent(pathname.split("/").filter(Boolean).pop() ?? ""); 24 25 const { getToken } = useAuth(); 25 26 const router = useRouter(); 26 27 const [lexicon, setLexicon] = useState<LexiconDetail | null>(null);