-
Notifications
You must be signed in to change notification settings - Fork 46
feat: deploy Noblocks as Farcaster Mini App using MiniKit #211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 28 commits
36603aa
9f70407
97e9faf
81545cc
f59b85c
a0b2138
68684f8
2c01141
81c441f
6ac4e1d
496b649
810611d
97ac2fa
8b7187b
ef0f467
eb50745
010ec2d
15e07aa
06f7b5e
f246d19
79c674b
d1b3bf8
e9453ef
659b567
56a458b
6867095
3f9e619
0db23ca
cd1e447
01ce454
eb71d9f
5fc648c
e75cbfe
4e80c89
cb730ab
809d079
8dc42de
1e444b9
df37353
9a866fd
3979b4e
3a24a87
e3a1c4b
86f7b6d
4125602
8b2b01e
7fa1f47
8f2e79e
05ea5e8
e0a08fa
2ed678c
ce52326
fa99608
b323cb6
d9d9563
a91bd08
a1f9376
ff5635a
9095ff4
0d1b93b
05bccf0
4035478
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| { | ||
| "name": "Noblocks", | ||
| "description": "A crypto-enabled mini app for sending and receiving stablecoins on Base using Farcaster.", | ||
| "app": { | ||
| "url": "https://noblocks.xyz", | ||
| "window": { | ||
| "height": 600, | ||
| "width": 400 | ||
| } | ||
| }, | ||
| "icon": { | ||
| "light": "https://noblocks.xyz/icons/favicon_.png", | ||
| "dark": "https://noblocks.xyz/icons/favicon.png" | ||
| }, | ||
|
||
| "splash_screen": { | ||
| "light": "https://noblocks.xyz/android-chrome-192x192.png", | ||
| "dark": "https://noblocks.xyz/android-chrome-192x192.png" | ||
| }, | ||
| "navigation_bar": { | ||
| "visible": true, | ||
| "title": "Noblocks" | ||
| }, | ||
| "version": "1.0.0", | ||
| "primaryCategory": "finance", | ||
| "tags": ["web3"], | ||
| "developer": { | ||
| "name": "CONSOLATION LOTACHI", | ||
| "website": "https://github.com/KEM-CONSOLATION", | ||
| "contact": "mailto:[email protected]" | ||
| }, | ||
KEM-CONSOLATION marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| "accountAssociation": { | ||
| "header": "eyJmaWQiOjExNjI4ODAsInR5cGUiOiJhdXRoIiwia2V5IjoiMHgzZERCMTQ5RGNBMEZjMDcxM0ZjMzhBQ2IxNjdDMzM2NzIwQjVGMDdmIn0", | ||
| "payload": "eyJkb21haW4iOiJub2Jsb2Nrcy1pYWxjLnZlcmNlbC5hcHAifQ", | ||
| "signature": "qV6dj7aiufYm6/Lnb79FhFQD3jETpubKivIbqXyFVdZK8kvLOsE/bMw0b3YN2iTbp44RAD83IXt0mJVjW8NuVRw=" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,10 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function GET() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hostedManifestUrl = process.env.NEXT_PUBLIC_FC_HOSTED_MANIFEST_URL!; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Response(null, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: 307, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Location: hostedManifestUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Cache-Control": "public, s-maxage=300, stale-while-revalidate=86400", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function GET() { | |
| const hostedManifestUrl = process.env.NEXT_PUBLIC_FC_HOSTED_MANIFEST_URL!; | |
| return new Response(null, { | |
| status: 307, | |
| headers: { | |
| Location: hostedManifestUrl, | |
| "Cache-Control": "public, s-maxage=300, stale-while-revalidate=86400", | |
| }, | |
| }); | |
| } | |
| export async function GET() { | |
| const hostedManifestUrl = process.env.NEXT_PUBLIC_FC_HOSTED_MANIFEST_URL; | |
| if (!hostedManifestUrl) { | |
| return new Response("Missing NEXT_PUBLIC_FC_HOSTED_MANIFEST_URL", { status: 500 }); | |
| } | |
| try { | |
| new URL(hostedManifestUrl); | |
| } catch { | |
| return new Response("Invalid NEXT_PUBLIC_FC_HOSTED_MANIFEST_URL", { status: 500 }); | |
| } | |
| return new Response(null, { | |
| status: 307, | |
| headers: { | |
| Location: hostedManifestUrl, | |
| "Cache-Control": "public, max-age=300, s-maxage=300, stale-while-revalidate=86400", | |
| }, | |
| }); | |
| } | |
| export async function HEAD() { | |
| return GET(); | |
| } |
🤖 Prompt for AI Agents
In app/.well-known/route.ts around lines 1–10, remove the non-null assertion and
add a guard that checks process.env.NEXT_PUBLIC_FC_HOSTED_MANIFEST_URL exists
and is a valid URL (e.g., try new URL(value) and catch errors); if
missing/invalid, return a 400 (or 500) Response with a clear message instead of
performing the redirect; when valid, use the parsed URL.href as the Location
header and update Cache-Control to include a browser-facing max-age as well as
s-maxage and stale-while-revalidate (for example: "public, max-age=60,
s-maxage=300, stale-while-revalidate=86400").
💡 Verification agent
🧩 Analysis chain
Route path doesn’t intercept /.well-known/farcaster.json
This route handles /.well-known, not /.well-known/farcaster.json. If you intend to redirect the JSON path via code (instead of vercel.json), move this file under app/.well-known/farcaster.json/route.ts. Otherwise, delete this route and rely on the Vercel redirect only.
I can script a curl check to confirm the live behavior for both /.well-known and /.well-known/farcaster.json.
🏁 Script executed:
#!/bin/bash
# Locate vercel.json
echo "=== vercel.json path ==="
fd -t f vercel.json || echo "vercel.json not found"
# Show content of vercel.json if exists
if [ -f vercel.json ]; then
echo "=== vercel.json content ==="
sed -n '1,200p' vercel.json
fi
# Locate next.config.js or next.config.ts
echo "=== next.config.js or ts ==="
fd -t f next.config.js next.config.ts || echo "next.config.js/ts not found"
# Show redirect config in next.config.js
if [ -f next.config.js ]; then
echo "=== next.config.js redirects ==="
grep -R "redirects" -n next.config.js | sed 's/^/ /'
fi
# Show redirect config in next.config.ts
if [ -f next.config.ts ]; then
echo "=== next.config.ts redirects ==="
grep -R "redirects" -n next.config.ts | sed 's/^/ /'
fi
# Show app/.well-known/route.ts content
echo "=== app/.well-known/route.ts content ==="
if [ -f app/.well-known/route.ts ]; then
sed -n '1,200p' app/.well-known/route.ts
else
echo "app/.well-known/route.ts not found"
fiLength of output: 781
Route file only matches /.well-known, not /.well-known/farcaster.json
Move app/.well-known/route.ts to app/.well-known/farcaster.json/route.ts if you intend to handle the JSON redirect in code; otherwise delete this route and rely on the existing vercel.json redirect for the .well-known/farcaster.json path.
🤖 Prompt for AI Agents
In app/.well-known/route.ts around lines 1 to 10, the route currently only
matches /.well-known; either move this file to
app/.well-known/farcaster.json/route.ts so it handles requests to
/.well-known/farcaster.json (keeping the same GET handler) or delete this route
file entirely and rely on the existing vercel.json redirect for the
/.well-known/farcaster.json path; after moving, ensure the file path and exports
remain the same and update any imports or references if necessary.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import { ImageResponse } from "next/og"; | ||
|
|
||
| export async function GET() { | ||
| return new ImageResponse( | ||
| ( | ||
| <div | ||
| style={{ | ||
| display: "flex", | ||
| height: "100%", | ||
| width: "100%", | ||
| alignItems: "center", | ||
| justifyContent: "center", | ||
| flexDirection: "column", | ||
| background: "linear-gradient(135deg, #317EFB 0%, #1E40AF 100%)", | ||
| color: "white", | ||
| fontFamily: "system-ui, -apple-system, sans-serif", | ||
| padding: "40px", | ||
| }} | ||
| > | ||
| {/* Logo/Brand */} | ||
| <div | ||
| style={{ | ||
| fontSize: 80, | ||
| fontWeight: "800", | ||
| marginBottom: 20, | ||
| letterSpacing: "-0.02em", | ||
| }} | ||
| > | ||
| Noblocks | ||
| </div> | ||
|
|
||
| {/* Tagline */} | ||
| <div | ||
| style={{ | ||
| fontSize: 36, | ||
| textAlign: "center", | ||
| maxWidth: "900px", | ||
| lineHeight: 1.2, | ||
| opacity: 0.95, | ||
| fontWeight: "400", | ||
| }} | ||
| > | ||
| Change stablecoins to cash in seconds | ||
| </div> | ||
|
|
||
| {/* Subtitle */} | ||
| <div | ||
| style={{ | ||
| fontSize: 24, | ||
| textAlign: "center", | ||
| maxWidth: "800px", | ||
| marginTop: 30, | ||
| opacity: 0.8, | ||
| fontWeight: "300", | ||
| }} | ||
| > | ||
| Decentralized payments to any bank or mobile wallet | ||
| </div> | ||
|
|
||
| {/* Visual element */} | ||
| <div | ||
| style={{ | ||
| position: "absolute", | ||
| bottom: 40, | ||
| right: 40, | ||
| display: "flex", | ||
| alignItems: "center", | ||
| fontSize: 20, | ||
| opacity: 0.7, | ||
| }} | ||
| > | ||
| noblocks.xyz | ||
| </div> | ||
| </div> | ||
| ), | ||
| { | ||
| width: 1200, | ||
| height: 630, | ||
| }, | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||||||||||||||||||||||||||
| import DetailClient from "@/app/components/blog/post/detail-client"; | ||||||||||||||||||||||||||||||
| import { getPost, getRecentPosts } from "@/app/lib/sanity-data"; | ||||||||||||||||||||||||||||||
| import { notFound } from "next/navigation"; | ||||||||||||||||||||||||||||||
| import { notFound, redirect } from "next/navigation"; | ||||||||||||||||||||||||||||||
| import { Metadata } from "next"; | ||||||||||||||||||||||||||||||
| import type { PortableTextBlock } from "@portabletext/types"; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -75,9 +75,17 @@ export async function generateMetadata({ | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| export default async function BlogPostDetailPage({ | ||||||||||||||||||||||||||||||
| params, | ||||||||||||||||||||||||||||||
| searchParams, | ||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||
| params: Promise<{ id: string }>; | ||||||||||||||||||||||||||||||
| searchParams: Promise<{ mini?: string }>; | ||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||
|
Comment on lines
110
to
116
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Use canonical Next.js App Router types for params/searchParams These aren’t Promises in App Router. Aligning types removes unnecessary awaits and TS friction. -export default async function BlogPostDetailPage({
- params,
- searchParams,
-}: {
- params: Promise<{ id: string }>;
- searchParams: Promise<{ mini?: string }>;
-}) {
+export default async function BlogPostDetailPage({
+ params,
+ searchParams,
+}: {
+ params: { id: string };
+ searchParams?: { mini?: string };
+}) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| // Redirect to home page if in mini mode | ||||||||||||||||||||||||||||||
| const resolvedSearchParams = await searchParams; | ||||||||||||||||||||||||||||||
| if (resolvedSearchParams.mini === "true") { | ||||||||||||||||||||||||||||||
| redirect("/?mini=true"); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const { id } = await params; | ||||||||||||||||||||||||||||||
| const post = await getPost(id); | ||||||||||||||||||||||||||||||
| if (!post) notFound(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ import React, { Suspense } from "react"; | |||||||||||||||||||||||||||||||||||||||
| import { getPosts, getCategories, getCachedPosts } from "@/app/lib/sanity-data"; | ||||||||||||||||||||||||||||||||||||||||
| import HomeClient from "@/app/components/blog/home-client"; | ||||||||||||||||||||||||||||||||||||||||
| import { Metadata } from "next"; | ||||||||||||||||||||||||||||||||||||||||
| import { redirect } from "next/navigation"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Force dynamic rendering to ensure fresh data | ||||||||||||||||||||||||||||||||||||||||
| export const dynamic = "force-dynamic"; | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -37,7 +38,17 @@ export async function generateMetadata(): Promise<Metadata> { | |||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export default async function Home() { | ||||||||||||||||||||||||||||||||||||||||
| export default async function Home({ | ||||||||||||||||||||||||||||||||||||||||
| searchParams, | ||||||||||||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||||||||||||
| searchParams: Promise<{ mini?: string }>; | ||||||||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||||||||
| // Redirect to home page if in mini mode | ||||||||||||||||||||||||||||||||||||||||
| const resolvedSearchParams = await searchParams; | ||||||||||||||||||||||||||||||||||||||||
| if (resolvedSearchParams.mini === "true") { | ||||||||||||||||||||||||||||||||||||||||
| redirect("/?mini=true"); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+47
to
+56
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Fix searchParams typing; it isn’t a Promise in Next.js App Router. Current type compiles but is incorrect and can confuse tools. -export default async function Home({
- searchParams,
-}: {
- searchParams: Promise<{ mini?: string }>;
-}) {
- // Redirect to home page if in mini mode
- const resolvedSearchParams = await searchParams;
- if (resolvedSearchParams.mini === "true") {
+export default async function Home({
+ searchParams,
+}: {
+ searchParams?: { mini?: string };
+}) {
+ // Redirect to home page if in mini mode
+ if (searchParams?.mini === "true") {
redirect("/?mini=true");
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Fetch data from Sanity (cached to avoid duplicate fetch in metadata) | ||||||||||||||||||||||||||||||||||||||||
| const sanityPosts = await getCachedPosts(); | ||||||||||||||||||||||||||||||||||||||||
| const sanityCategories = await getCategories(); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.