{"id":11888,"date":"2026-10-09T09:02:46","date_gmt":"2026-10-09T00:02:46","guid":{"rendered":"https:\/\/www.freelifemakers.org\/wordpress\/?p=11888"},"modified":"2026-10-09T21:26:52","modified_gmt":"2026-10-09T12:26:52","slug":"nextjs-sns-server-50","status":"publish","type":"post","link":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2026\/10\/09\/nextjs-sns-server-50\/","title":{"rendered":"[nextjs]SNS Server-50(myapp60)\u00a0"},"content":{"rendered":"\n<p>\ud83d\udc49\ud83c\udffb myapp60\uc5d0\uc11c\ub294 \ud398\uc774\uc9c0\ub124\uc774\uc158 \uae30\ub2a5 \ucd94\uac00\ub85c \uc778\ud574 \ubc1c\uc0dd\ud55c \uc88b\uc544\uc694,\ubd80\uc2a4\ud2b8 \ubd80\ubd84 \uc624\ub958\ub97c \uc218\uc815\ud569\ub2c8\ub2e4.<br>In myapp60, we are fixing errors related to the &#8220;Like&#8221; and &#8220;Boost&#8221; features that arose from the addition of pagination.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \ud398\uc774\uc9c0\ub124\uc774\uc158 \ubd80\ubd84\uc744 \uc644\ub8cc \ud569\ub2c8\ub2e4.<br>I will complete the pagination section.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb  \uc5d1\ud2f0\ube0c\ud14c\ub9c8 \uc790\ub3d9\uc804\ud658\uc774 \ub418\uc9c0 \uc54a\ub294 \ubb38\uc81c\ub97c \uc218\uc815\ud569\ub2c8\ub2e4.<br>Fixes an issue where the active theme does not switch automatically.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \uc804\uccb4 \ucf54\ub4dc\ub294 \uae43\ud5c8\ube0c\uc5d0\uc11c \ud655\uc778 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<br>You can find the full code on GitHub.<\/p>\n\n\n\n<p><a href=\"https:\/\/github.com\/gideonslife01\/flm-nextjs\">https:\/\/github.com\/gideonslife01\/flm-nextjs<\/a><\/p>\n\n\n\n<p>\ud83d\udcc1 \uc804\uccb4 \ud504\ub85c\uc81d\ud2b8 \uad6c\uc870 \/ Overall Project Structure<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>myapp project\/  \n\u251c\u2500\u2500 app\/  (Next.js App Router)\n\u2502   \u251c\u2500\u2500 .well-known\/webfinger\/route.ts  -&gt; webfinger\n\u2502   \u251c\u2500\u2500 hooks\/useCursorPagination.ts    -&gt; Pagination Hook\n\u2502   \u251c\u2500\u2500 api\/follow\/route.ts    -&gt; Follow API(temporary)\n\u2502   \u251c\u2500\u2500 api\/announce\/route.ts  -&gt; Boost(Announcement)\n\u2502   \u251c\u2500\u2500 api\/like\/route.ts.     -&gt; Like API\n\u2502   \u251c\u2500\u2500 api\/posts\/route.ts     -&gt; Writing API(deprecated)\n\u2502   \u251c\u2500\u2500 api\/timeline\/route.ts  -&gt; Timeline API\n\u2502   \u251c\u2500\u2500 api\/followinglist\/route.ts  -&gt; followinglist API\n\u2502   \u251c\u2500\u2500 api\/followerslist\/route.ts  -&gt; followerslist API\n\u2502   \u251c\u2500\u2500 <span style=\"background-color: initial; font-size: inherit; text-align: initial; color: initial;\">api\/v1\/instance<\/span>\/route.ts -&gt; auth\n\u2502   \u251c\u2500\u2500 api\/v1\/apps\/route.ts.    -&gt; auth\n\u2502   \u251c\u2500\u2500 api\/v1\/accounts\/verify_credentials\/route.ts -&gt; auth\n\u2502   \u251c\u2500\u2500 api\/v1\/statuses\/route.ts -&gt; Write Post\n\u2502   \u251c\u2500\u2500 <strong><span style=\"background-color: initial; font-size: inherit; text-align: initial; color: initial;\">api\/v1\/timelines\/_home_back\/route.ts<\/span> -&gt; timeline(Depreacted)<\/strong>\n\u2502   \u251c\u2500\u2500 api\/v1\/media\/route.ts -&gt; Image upload\n\u2502   \u251c\u2500\u2500 api\/v1\/media\/&#91;id]\/route.ts  -&gt; Image Delete \n\u2502   \u251c\u2500\u2500 api\/v1\/instance\/rules\/route.ts -&gt; Feature not implemented.\n\u2502   \u251c\u2500\u2500 api\/v1\/instance\/domain_blocks\/route.ts -&gt; Feature not implemented.\n\u2502   \u251c\u2500\u2500 api\/v1\/filters\/route.ts -&gt; Feature not implemented.\n\u2502   \u251c\u2500\u2500 api\/v1\/lists\/route.ts -&gt; Feature not implemented.\n\u2502   \u251c\u2500\u2500 api\/v1\/announcements\/route.ts -&gt; Feature not implemented.\n\u2502   \u251c\u2500\u2500 <strong>api\/v1\/timelines\/_public_back\/route.ts -&gt; Feature not implemented.(Deprecated)<\/strong>\n\u2502   \u251c\u2500\u2500 api\/v1\/timelines\/&#91;type]\/route.ts -&gt; Timeline API\n\u2502   \u251c\u2500\u2500 api\/v1\/statuses\/&#91;id]\/context\/route.ts -&gt; Post Check,Delete\n\u2502   \u251c\u2500\u2500 <span style=\"background-color: initial; font-size: inherit; text-align: initial; color: initial;\">oauth\/authorize\/route.ts<\/span> -&gt; auth\n\u2502   \u251c\u2500\u2500 <span style=\"background-color: initial; font-size: inherit; text-align: initial; color: initial;\">oauth\/token\/route.ts<\/span> -&gt; auth\n\u2502   \u251c\u2500\u2500 <span style=\"background-color: initial; font-size: inherit; text-align: initial; color: initial;\">oauth\/login-and-authorize\/route.ts<\/span> -&gt; Tursky,Pinafore login processing\n\u2502   \u251c\u2500\u2500 oauth\/revoke\/route.ts -&gt; Feature not implemented.\n\u2502   \u251c\u2500\u2500 <span style=\"background-color: initial; font-size: inherit; text-align: initial; color: initial;\">api\/v1\/search\/home\/route.ts<\/span> -&gt; search\n\u2502   \u251c\u2500\u2500 <span style=\"background-color: initial; font-size: inherit; text-align: initial; color: initial;\">api\/v2\/search\/home\/route.ts<\/span> -&gt; search\n\u2502   \u251c\u2500\u2500 api\/v2\/notifications\/policy\/route.ts-&gt; Feature not implemented.\n\u2502   \u251c\u2500\u2500 api\/v1\/accounts\/&#91;id]\/followers\/route.ts -&gt; followers\n\u2502   \u251c\u2500\u2500 api\/v1\/accounts\/&#91;id]\/following\/route.ts -&gt; following\n\u2502   \u251c\u2500\u2500 api\/v1\/statuses\/&#91;id]\/route.ts  -&gt; Post delete(pinafore)\n\u2502   \u251c\u2500\u2500 <strong>api\/v1\/statuses\/&#91;id]\/favourite\/route.ts<\/strong> <strong>-&gt; favourite(for pinafore)<\/strong>\n\u2502   \u251c\u2500\u2500 <strong>api\/v1\/statuses\/&#91;id]\/unfavourite\/route.ts<\/strong> <strong>-&gt; unfavourite(for pinafore)<\/strong>\n\u2502   \u251c\u2500\u2500 <strong>api\/v1\/statuses\/&#91;id]\/reblog\/route.ts <\/strong> <strong>-&gt; reblog&#91;boost](for pinafore)<\/strong>\n\u2502   \u251c\u2500\u2500 <strong>api\/v1\/statuses\/&#91;id]\/unreblog\/route.ts <\/strong> <strong>-&gt; unreblog&#91;unboost](for pinafore)<\/strong>\n\u2502   \u251c\u2500\u2500 api\/auth\/signup\/route.ts        -&gt; Website Signup\n\u2502   \u251c\u2500\u2500 api\/auth\/login\/route.ts         -&gt; Website Login\n\u2502   \u251c\u2500\u2500<strong> <\/strong>api\/auth\/logout\/route.ts        -&gt; Website Logout\n\u2502   \u251c\u2500\u2500<strong> <\/strong>api\/auth\/refresh\/route.ts       -&gt; Refresh Token\n\u2502   \u251c\u2500\u2500<strong> <\/strong>api\/cleanup-orphan\/route.ts     -&gt; post,outbox clean up\n\u2502   \u251c\u2500\u2500<strong> <\/strong>api\/auth\/me\/route.tsx           -&gt; Login Check<strong>\n<\/strong>\u2502   \u251c\u2500\u2500 auth\/signup\/page.tsx            -&gt; Signup UI\n\u2502   \u251c\u2500\u2500 auth\/signup\/page.tsx            -&gt; Login UI        \n\u2502   \u251c\u2500\u2500 users\/&#91;username]\/\n\u2502   \u2502   \u251c\u2500\u2500 statuses\/&#91;id]\/route.ts -&gt; Indivisual Post\n\u2502   \u2502   \u251c\u2500\u2500 route.ts               -&gt; Acotr Information\n\u2502   \u2502   \u251c\u2500\u2500 followers\/route.ts     -&gt; Followers List\n\u2502   \u2502   \u251c\u2500\u2500 following\/route.ts     -&gt; Following List\n\u2502   \u2502   \u251c\u2500\u2500 inbox\/route.ts         -&gt; Inbox\n\u2502   \u2502   \u2514\u2500\u2500 outbox\/route.ts        -&gt; outbox\n\u2502   \u251c\u2500\u2500 usersui\/&#91;username]\/\n\u2502   \u2502   \u251c\u2500\u2500 page.tsx               -&gt; Timeline UI(Server, 404)\n\u2502   \u2502   \u251c\u2500\u2500 ClientPage.tsx.        -&gt; Timeline UI(Client,Call Active Themes)\n\u2502   \u2502   \u2514\u2500\u2500 _components\/themes\/\n\u2502   \u2502       \u251c\u2500\u2500 themeex\/ThemeexTheme.tsx      -&gt; Example Theme\n\u2502   \u2502       \u251c\u2500\u2500 themeex\/FollowersList.tsx     -&gt; Theme Example, FollowersList\n\u2502   \u2502       \u251c\u2500\u2500 themeex\/FollowingList.tsx     -&gt; Theme Example, FollowingList\n\u2502   \u2502       \u251c\u2500\u2500 themeex\/NotificationsList.tsx -&gt; Theme Example, NotificationsList\n\u2502   \u2502       \u251c\u2500\u2500 themeex\/TimelineList.tsx      -&gt; Theme Example, TimelineList\n\u2502   \u2502       \u251c\u2500\u2500 pinafore\/PinaforeTheme.tsx -&gt; Theme 1\n\u2502   \u2502       \u251c\u2500\u2500 pinafore\/FollowersList.tsx -&gt; Theme 1, FollowersList\n\u2502   \u2502       \u251c\u2500\u2500 pinafore\/FollowingList.tsx -&gt; Theme 1, FollowingList\n\u2502   \u2502       \u251c\u2500\u2500 pinafore\/NotificationsList.tsx -&gt; Theme 1, NotificationsList\n\u2502   \u2502       \u251c\u2500\u2500 pinafore\/TimelineList.tsx  -&gt; Theme 1, TimelineList\n\u2502   \u2502       \u251c\u2500\u2500 mastodon\/MastodonTheme.tsx -&gt; Theme 2\n\u2502   \u2502       \u2514\u2500\u2500 minimal\/MinimaltTheme.tsx  -&gt; Theme 3\n\u2502   \u251c\u2500\u2500 layout.tsx, page.tsx, globals.css\n\u2502   \u2514\u2500\u2500 favicon.ico\n\u251c\u2500\u2500 lib\/\n\u2502   \u251c\u2500\u2500 theme.tsx              -&gt; Theme Provider\n\u2502   \u251c\u2500\u2500 watchThemes.ts         -&gt; Check real-time theme changes\n\u2502   \u251c\u2500\u2500 auth.ts                -&gt; Authentication, User Management\n\u2502   \u251c\u2500\u2500 ap.ts                  -&gt; Follow,Undo,Create,Likes,Announce\n\u2502   \u251c\u2500\u2500 visibility.ts          -&gt; Visibility(Public,Unlisted,Private,Direct)\n\u2502   \u2514\u2500\u2500 db.ts                  -&gt; DB connection\n\u251c\u2500\u2500 data\/\n\u2502   \u251c\u2500\u2500 keys\/userIDs\/          -&gt; private.pem, public.pem(New)\n\u2502   \u2514\u2500\u2500 keys\/                  -&gt; private.pem, public.pem(legacy)\n\u251c\u2500\u2500 data.sqlite                -&gt; Database(1\/3)\n\u251c\u2500\u2500 data.sqlite-wal            -&gt; Database(2\/3)\n\u251c\u2500\u2500 data.sqlite-shm            -&gt; Database(3\/3)\n\u251c\u2500\u2500 Caddyfile                  -&gt; https \n\u251c\u2500\u2500 instrumentation.ts         -&gt; Background Server\n\u251c\u2500\u2500 proxy.ts                   -&gt; middleware.ts - &gt;proxy.ts(for Nextjs16)\n\u251c\u2500\u2500 postcss.config.mjs         -&gt; for Nextjs16\n\u2514\u2500\u2500 package.json<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \ud504\ub85c\uc81d\ud2b8 \uc2dc\uc791 \/ Project Start<\/p>\n\n\n\n<figure class=\"wp-block-embed is-type-wp-embed is-provider-freelifemakers-org wp-block-embed-freelifemakers-org\"><div class=\"wp-block-embed__wrapper\">\n<blockquote class=\"wp-embedded-content\" data-secret=\"JJcrI2VajW\"><a href=\"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2026\/09\/03\/nextjs-project-summary\/\">[nextjs]\ud504\ub85c\uc81d\ud2b8\uc694\uc57d\/project summary(myapp30+)<\/a><\/blockquote><iframe loading=\"lazy\" class=\"wp-embedded-content\" sandbox=\"allow-scripts\" security=\"restricted\" style=\"position: absolute; visibility: hidden;\" title=\"&#8220;[nextjs]\ud504\ub85c\uc81d\ud2b8\uc694\uc57d\/project summary(myapp30+)&#8221; &#8212; freelifemakers.org\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2026\/09\/03\/nextjs-project-summary\/embed\/#?secret=juOJZY2rpu#?secret=JJcrI2VajW\" data-secret=\"JJcrI2VajW\" width=\"560\" height=\"315\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\"><\/iframe>\n<\/div><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>\ud83d\udcc1 \uc88b\uc544\uc694 + \ubd80\uc2a4\ud2b8 \uc218\uc815 \/ Modify Like + Boost<\/p>\n\n\n\n<p>\u2714\ufe0f \ub77c\uc6b0\ud2b8 \ucd94\uac00 \/ Add route<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\napp\/api\/v1\/statuses\/&#91;id]\/favourite\/route.ts\n\napp\/api\/v1\/statuses\/&#91;id]\/unfavourite\/route.ts \n\napp\/api\/v1\/statuses\/&#91;id]\/reblog\/route.ts \n\napp\/api\/v1\/statuses\/&#91;id]\/unreblog\/route.ts\n<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f app\/api\/v1\/statuses\/[id]\/favourite\/route.ts<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>\/\/ \u2705 myapp60 - favorite for pinafore\n\nexport const dynamic = 'force-dynamic';\nimport { NextRequest, NextResponse } from 'next\/server';\nimport db from '@\/lib\/db';\nimport { getActorData, sendLike, signedFetch } from '@\/lib\/ap';\nconst DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';\n\nfunction getUser(req: Request) {\n  const auth = req.headers.get('Authorization')?.replace('Bearer ','').trim();\n  if (auth) {\n    const o = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(auth) as any;\n    if (o?.username) return o.username;\n  }\n  const cookie = req.headers.get('cookie') || '';\n  const m = cookie.match(\/refresh_token=(&#91;^;]+)\/);\n  if (m) {\n    const s = db.prepare('SELECT username FROM sessions WHERE refresh_token=?').get(m&#91;1]) as any;\n    if (s?.username) return s.username;\n  }\n  return null;\n}\n\nexport async function POST(req: NextRequest, { params }: { params: Promise&lt;{id: string}&gt; }) {\n  const authUser = getUser(req);\n  if (!authUser) return NextResponse.json({ error: 'Unauthorize' }, { status: 401 });\n  const username = authUser;\n  const { id } = await params;\n\n  let target = id;\n  let post: any = db.prepare(`SELECT * FROM posts WHERE id=?`).get(id) as any;\n  if (post) {\n    target = `https:\/\/${DOMAIN}\/users\/${post.username}\/statuses\/${post.id}`;\n  } else {\n    post = db.prepare(`SELECT * FROM inbox_posts WHERE id=? OR original_id=? OR original_id LIKE '%' ||?|| '%'`).get(id, id, id) as any;\n    if (post?.original_id) target = post.original_id;\n    else if (post) target = post.original_id || id;\n    else target = id.startsWith('http')? id : `https:\/\/${DOMAIN}\/users\/${username}\/statuses\/${id}`;\n  }\n\n  const myActor = `https:\/\/${DOMAIN}\/users\/${username}`;\n  let apObjectId = target.replace('\/posts\/', '\/statuses\/');\n  let inbox: string | null = null;\n  const isOwn = apObjectId.includes(`${DOMAIN}\/users\/${username}\/`);\n\n  if (!isOwn) {\n    try {\n      const pr = await signedFetch(apObjectId, username);\n      if (pr.ok) {\n        const d = await pr.json();\n        apObjectId = (d.id || apObjectId).replace('\/posts\/', '\/statuses\/');\n        const info = await getActorData((d.attributedTo || d.actor) as any, username);\n        inbox = info.inbox;\n      }\n    } catch {}\n  }\n\n  const shortId = apObjectId.split('\/').pop()!.split('?')&#91;0].split('#')&#91;0];\n  const existing = db.prepare(`SELECT id FROM likes WHERE actor=? AND object LIKE '%' ||?|| '%'`).get(myActor, shortId) as any;\n  if (existing) {\n    const c = (db.prepare(`SELECT COUNT(*) as c FROM likes WHERE object LIKE '%' ||?|| '%'`).get(shortId) as any).c;\n    return NextResponse.json({ id: id, favourited: true, favourites_count: c, reblogged: false });\n  }\n\n  let likeDoc: any;\n  try {\n    if (isOwn ||!inbox) {\n      likeDoc = { id: `${myActor}\/likes\/${crypto.randomUUID()}`, actor: myActor, object: apObjectId };\n    } else {\n      likeDoc = (await sendLike(inbox!, apObjectId, username)).likeDoc;\n    }\n  } catch (e: any) {\n    console.log(`\u26a0 sendLike \uc2e4\ud328\/sendLike failed, \ub85c\uceec\uc5d0\ub9cc \uc800\uc7a5\/Save locally only: ${e.message}`);\n    likeDoc = { id: `${myActor}\/likes\/${crypto.randomUUID()}`, actor: myActor, object: apObjectId };\n  }\n\n  db.prepare('INSERT OR IGNORE INTO likes (id, actor, object, username) VALUES (?,?,?,?)')\n  .run(likeDoc.id, myActor, likeDoc.object.replace('\/posts\/', '\/statuses\/'), username);\n\n  const c = (db.prepare(`SELECT COUNT(*) as c FROM likes WHERE object LIKE '%' ||?|| '%'`).get(shortId) as any).c;\n console.log(`\ud83d\udc9c v1-favourite`);\n  return NextResponse.json({\n    id: id,\n    favourited: true,\n    favourites_count: c,\n    reblogged: false,\n    account: { id: username },\n  });\n}<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f app\/api\/v1\/statuses\/[id]\/unfavourite\/route.ts <\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ \u2705 myapp60 - for pinafore - unfavourite\n\nexport const dynamic = 'force-dynamic';\nimport { NextRequest, NextResponse } from 'next\/server';\nimport db from '@\/lib\/db';\nimport { getActorData, sendUndoLike, signedFetch } from '@\/lib\/ap';\nconst DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';\n\nfunction getUser(req: Request) {\n  const auth = req.headers.get('Authorization')?.replace('Bearer ','').trim();\n  if (auth) {\n    const o = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(auth) as any;\n    if (o?.username) return o.username;\n  }\n  const cookie = req.headers.get('cookie') || '';\n  const m = cookie.match(\/refresh_token=(&#91;^;]+)\/);\n  if (m) {\n    const s = db.prepare('SELECT username FROM sessions WHERE refresh_token=?').get(m&#91;1]) as any;\n    if (s?.username) return s.username;\n  }\n  return null;\n}\n\nexport async function POST(req: NextRequest, { params }: { params: Promise&lt;{id: string}&gt; }) {\n  const authUser = getUser(req);\n  if (!authUser) return NextResponse.json({ error: 'Unauthorize' }, { status: 401 });\n  const username = authUser;\n  const { id } = await params;\n\n  let target = id;\n  let post: any = db.prepare(`SELECT * FROM posts WHERE id=?`).get(id) as any;\n  if (post) {\n    target = `https:\/\/${DOMAIN}\/users\/${post.username}\/statuses\/${post.id}`;\n  } else {\n    post = db.prepare(`SELECT * FROM inbox_posts WHERE id=? OR original_id=? OR original_id LIKE '%' ||?|| '%'`).get(id, id, id) as any;\n    if (post?.original_id) target = post.original_id;\n    else target = id.startsWith('http')? id : `https:\/\/${DOMAIN}\/users\/${username}\/statuses\/${id}`;\n  }\n\n  const myActor = `https:\/\/${DOMAIN}\/users\/${username}`;\n  let apObjectId = target.replace('\/posts\/', '\/statuses\/');\n  let inbox: string | null = null;\n  const isOwn = apObjectId.includes(`${DOMAIN}\/users\/${username}\/`);\n\n  if (!isOwn) {\n    try {\n      const pr = await signedFetch(apObjectId, username);\n      if (pr.ok) {\n        const d = await pr.json();\n        apObjectId = (d.id || apObjectId).replace('\/posts\/', '\/statuses\/');\n        const info = await getActorData((d.attributedTo || d.actor) as any, username);\n        inbox = info.inbox;\n      }\n    } catch {}\n  }\n\n  const shortId = apObjectId.split('\/').pop()!.split('?')&#91;0].split('#')&#91;0];\n  const likeRow = db.prepare(`SELECT id, object FROM likes WHERE actor=? AND object LIKE '%' ||?|| '%'`).get(myActor, shortId) as any;\n\n  if (likeRow &amp;&amp;!isOwn &amp;&amp; inbox) {\n    try {\n      await sendUndoLike(inbox, likeRow.id, likeRow.object, username);\n      console.log(`\ud83d\udc94 &#91;${username}] Undo Like \uc804\uc1a1 -&gt; ${inbox}`);\n    } catch (e: any) {\n      console.log(`\u26a0 Undo Like \uc2e4\ud328, \ub85c\uceec\ub9cc \uc0ad\uc81c: ${e.message}`);\n    }\n  }\n\n  if (likeRow) {\n    db.prepare(`DELETE FROM likes WHERE id=?`).run(likeRow.id);\n  } else {\n    \/\/ id\ub85c \uc9c1\uc811 \ub9e4\uce6d \uc548\ub418\uba74 shortId LIKE\ub85c \ud55c\ubc88 \ub354 \uc0ad\uc81c\n    \/\/ If direct matching by ID fails, perform an additional deletion using `shortId LIKE`.\n    db.prepare(`DELETE FROM likes WHERE actor=? AND object LIKE '%' ||?|| '%'`).run(myActor, shortId);\n  }\n\n  const c = (db.prepare(`SELECT COUNT(*) as c FROM likes WHERE object LIKE '%' ||?|| '%'`).get(shortId) as any).c;\n console.log(`\ud83d\udc94 v1-unfavourite`);\n  return NextResponse.json({\n    id: id,\n    favourited: false,\n    favourites_count: c,\n    reblogged: false,\n  });\n}<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f app\/api\/v1\/statuses\/[id]\/reblog\/route.ts &#8211; \uae43\ud5c8\ube0c \ucc38\uc870 \/ Refer to GitHub.<\/p>\n\n\n\n<p>\u2714\ufe0f app\/api\/v1\/statuses\/[id]\/unreblog\/route.ts &#8211; \uae43\ud5c8\ube0c \ucc38\uc870 \/ Refer to GitHub.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>\ud83d\udcc1 \ud14c\ub9c8 \ubcc0\uacbd \uc218\uc815 \/ Theme Change Modification<\/p>\n\n\n\n<p>\u2714\ufe0f \ud14c\ub9c8\uac00 \ubcc0\uacbd\ub418\uc5b4\ub3c4 Pinafore \ud14c\ub9c8\ub85c \uace0\uc815\ub418\ub294 \ubb38\uc81c\ub97c \uc218\uc815\ud588\uc2b5\ub2c8\ub2e4.<br>Fixed an issue where the theme remained locked to &#8220;Pinafore&#8221; even after the theme was changed.<\/p>\n\n\n\n<p>\u2714\ufe0f \uc0c8\ub85c\uc6b4 \ud14c\ub9c8\ub97c \ub9cc\ub4e4\uc5b4\uc11c \uc5c5\ub85c\ub4dc\ud558\uba74 \ud14c\ub9c8\ud30c\uc77c\uc774 \uc874\uc7ac\ud558\ub294\uc9c0 \ud655\uc778\ud558\uace0 index.ts\ud30c\uc77c\uc744 \uc0dd\uc131\ud569\ub2c8\ub2e4.<br>When you create and upload a new theme, the system checks for the existence of the theme file and generates an <code>index.ts<\/code> file.<\/p>\n\n\n\n<p>\u2714\ufe0f \ud14c\ub9c8\ub514\ub809\ud1a0\ub9ac \uba85\uc740 \uc790\uc720\ub86d\uac8c \uc218\uc815\uac00\ub2a5\ud558\uace0 \ud14c\ub9c8\ub514\ub809\ud1a0\ub9ac \ub0b4\uc758 \ud30c\uc77c\uc774\ub984\uc740 \ubaa8\ub450 \ub3d9\uc77c\ud558\uac8c \uc801\uc6a9\ud574\uc57c\ud569\ub2c8\ub2e4.<br>You are free to rename the theme directory, but the filenames within the directory must remain exactly the same.<\/p>\n\n\n\n<p>\u2714\ufe0f \ud14c\ub9c8\ub97c \uc0ad\uc81c\ud558\ub294 \uacbd\uc6b0\ub294 \uad00\ub9ac\uc790 \ubaa8\ub4dc\uc5d0\uc11c \uc0ad\uc81c\ud558\ub3c4\ub85d \ucc98\ub9ac\ud560 \uc608\uc815\uc785\ub2c8\ub2e4.<br>We plan to handle theme deletion via the administrator mode.<\/p>\n\n\n\n<p>&#8212; lib\/watchThemas.ts<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>for (const folderName of folders) {\n... ...\n      \/\/ write code to index.ts themeName.ts files\n      const indexContent = `'use client';\\n ${imports}\nexport const themeRegistry = {\n${registry}} as const;\nexport type ThemeName = keyof typeof themeRegistry;\nexport function getThemeComponent(name: ThemeName) {\n  return themeRegistry&#91;name]?.component;\n}\n`;\n      const namesContent = `export const themeNames = ${JSON.stringify(validFolders)} as const;\nexport type ThemeName = typeof themeNames&#91;number];\n`;\n}\n... ...  \n\n\/\/ \u2705 myapp60\n  if (process.env.NODE_ENV!== 'production') {\n    let t: NodeJS.Timeout;\n    chokidar.watch(themesDir, {\n      depth: 1,\n      ignoreInitial: true,\n      ignored: (p: string) =&gt; {\n        const b = path.basename(p);\n        return b === 'index.ts' || b === 'themeNames.ts';\n      }\n    }).on('all', (event) =&gt; {\n      \/\/ Check addDir, unlinkDir, add, unlink\n      clearTimeout(t);\n      t = setTimeout(generate, 1000); \/\/ \u2705 1\ucd08 \ub300\uae30 \/ Wait 1 second\n    });\n  }<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>\ud83d\udcc1 \ud398\uc774\uc9c0 \ub124\uc774\uc158 \ud14c\uc2a4\ud2b8 \/ Pagination Test<\/p>\n\n\n\n<p>&nbsp;\u2714\ufe0f \uc784\uc2dc\ub77c\uc6b0\ud2b8 \uc0dd\uc131 \/ Create temporary route<\/p>\n\n\n\n<p>&#8212; \ud55c\ubc88\uc5d0 \ub9ce\uc740 \uae00\uc744 \uc4f0\uace0 \ub3d9\uc2dc\uc0ad\uc81c\ud558\ub294 \ub77c\uc6b0\ud2b8 \uc785\ub2c8\ub2e4.<br>This is a route for writing multiple posts at once and deleting them simultaneously.<\/p>\n\n\n\n<p>1)api\/test\/bulk\/route.ts<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code># 1. \uae00 50\uac1c \uc0dd\uc131 (\ub85c\uadf8\uc778 \uc0c1\ud0dc\uc5d0\uc11c) \/ Generate 50 posts (while logged in)\ncurl -X POST https:\/\/aloy-horizon.duckdns.org\/api\/test\/bulk \\\n  -H \"Content-Type: application\/json\" \\\n  -b \"refresh_token=YOUR_TOKEN\" \\\n  -d '{\"count\":50}'\n\n# 2. \uc0ad\uc81c \/ delete\ncurl -X DELETE https:\/\/aloy-horizon.duckdns.org\/api\/test\/bulk \\\n  -H \"Content-Type: application\/json\" \\\n  -b \"refresh_token=YOUR_TOKEN\" \\<\/code><\/pre>\n\n\n\n<p>&#8212; \ub9ac\ubaa8\ud2b8 \uc11c\ubc84\uae00 \uac15\uc81c \uc0ad\uc81c \ub77c\uc6b0\ud2b8<br>Route for forced deletion of remote server posts<\/p>\n\n\n\n<p>1) api\/test\/nuke-remote\/route.ts<\/p>\n\n\n\n<p>2) \uae30\ubcf8\uc801\uc73c\ub85c \ub85c\uceec\uc11c\ubc84\uc640 \ub9ac\ubaa8\ud2b8 \uc11c\ubc84\uc758 \uae00\uc774 \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\uba74 \uc0ad\uc81c \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.<br>By default, you cannot delete the content if the versions on the local server and the remote server do not match.<\/p>\n\n\n\n<p>3) \uc544\ub798\ub294 \ub85c\uceec\uc11c\ubc84 \uae00\uc740 \uc5c6\ub294\ub370 \ub9ac\ubaa8\ud2b8 \uc11c\ubc84\ub9cc \uae00\uc774 \ub0a8\uc544 \uc788\ub294\uacbd\uc6b0 \uc0ad\uc81c \ud560 \uc218 \uc788\ub294 \ub77c\uc6b0\ud2b8 \uc785\ub2c8\ub2e4.<br>The following is a route that allows you to delete posts that exist only on the remote server but not on the local server.<\/p>\n\n\n\n<p>4) \uc784\uc2dc\ub85c \uc0ac\uc6a9\ud558\uace0 \ud504\ub85c\ub355\uc158\uc5d0\uc11c\ub294 \uc0ad\uc81c \ud574\uc57c\ud569\ub2c8\ub2e4.<br>It should be used temporarily and removed in the production environment.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code># 1. \uc0c1\ud0dc \ud655\uc778 \/ Check Status\ncurl -b \"refresh_token=YOUR_TOKEN\" https:\/\/aloy-horizon.duckdns.org\/api\/test\/nuke-remote\n\n# 2. \uae00 \uc0ad\uc81c \/ post delete\ncurl -X DELETE -b \"refresh_token=YOUR_TOKEN\" \\\n  \"https:\/\/aloy-horizon.duckdns.org\/api\/test\/nuke-remote?id=01913d88-51bd-4564-9f0a-820ec7e9fb0e\"\n<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f \ucf54\ub4dc \uc218\uc815  \/ Code Modification<\/p>\n\n\n\n<p>&#8212; app\/usersui\/[username]\/ClientPage.tsx<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>  \/\/ \u2705 myapp60 \n  \/\/ - \ud398\uc774\uc9c0\ub124\uc774\uc158\uc740 TimelineList\uac00 \ub2f4\ub2f9, \uc5ec\uae30\uc120 \ube48 \ubc30\uc5f4\ub85c \uc2dc\uc791\n  \/\/ - Pagination is handled by TimelineList; start with an empty array here.\n  useEffect(() =&gt; {\n    if (!username) return;\n    setTimeline(&#91;]);\n    setLoading(false);\n  }, &#91;username]);\n\n  const refreshTimeline = async () =&gt; {\n    if (!username) return;\n    const json = await fetch(`\/api\/v1\/timelines\/home?limit=20&amp;format=cursor&amp;t=${Date.now()}`, { cache: 'no-store', credentials: 'include' }).then(r =&gt; r.json());\n    const updated = Array.isArray(json)? json : json.data;\n    setTimeline(updated || &#91;]);\n  };<\/code><\/pre>\n\n\n\n<p>&#8212; app\/usersui\/[username]\/_components\/themes\/themeex\/MainTheme.tsx<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># \ubaa8\ub4e0 \ud14c\ub9c8\ud30c\uc77c\uc774\ub984\uc744 MainTheme.tsx\ub85c \ubcc0\uacbd\n# Rename all theme files to MainTheme.tsx.<\/code><\/pre>\n\n\n\n<p>&#8212; app\/usersui\/[username]\/ClientPage.tsx<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:14px\"><code>  const {\n    flatData,\n    fetchNextPage,\n    hasNextPage,\n    isFetchingNextPage,\n  } = useCursorPagination({\n    key: &#91;'timeline', tab],\n    initialData: initialTimeline.slice(0, 20), \/\/ \u2705 myapp60\n    fetcher: async (cursor) =&gt; {\n      const params = new URLSearchParams();\n      params.set('limit', '20');\n      if (cursor) params.set('max_id', cursor);\n      const res = await fetch(`\/api\/v1\/timelines\/${tab}?${params.toString()}`, { credentials: 'include' });\n      if (!res.ok) throw new Error('fetch fail');\n      const json = await res.json();\n      const arr = Array.isArray(json)? json : (json.data || json.statuses || &#91;]);\n      const withMine = arr.map((p:any) =&gt; ({\n      ...p,\n        isMine: p.username === currentUser || p.account?.username === currentUser || p.acct === currentUser\n      }));\n      return {\n        data: withMine,\n        nextCursor: withMine.length? withMine&#91;withMine.length-1].id : null,\n        hasMore: withMine.length &gt;= 20\n      };\n    },\n  });\n\n  \/\/ \u2705 myapp60 - flatData\ub9cc \uc0ac\uc6a9, initialTimeline \uc911\ubcf5 \uc0ac\uc6a9 \uc548 \ud568\n  const flat = (flatData || &#91;]).flat().filter(Boolean) as any&#91;];\n  const data = flat.length &gt; 0? flat : initialTimeline.filter((p:any) =&gt; p &amp;&amp; p.id);\n     \n... ... \n     {data.filter((p:any) =&gt; p &amp;&amp; p.id).map((p: any, idx: number) =&gt; {\n        const uniqueKey = `${p.id}-${idx}`;\n        const isMine = !!(currentUser &amp;&amp; (\n          p.isMine || p.username === currentUser || p.acct === currentUser || p.account?.username === currentUser\n        ));\n        return (\n... ...\n                {currentUser &amp;&amp; isMine &amp;&amp; (\n                  &lt;button onClick={() =&gt; onDelete(p.id)} className=\"text-&#91;#e0245e] ml-auto text-xs hover:underline\"&gt;\n                    \ud83d\uddd1 Delete\n                  &lt;\/button&gt;\n                )}\n... ...\n        );\n      })}<\/code><\/pre>\n\n\n\n<p>\ud83d\udcc1 \ud14c\uc2a4\ud2b8 \/ Test<\/p>\n\n\n\n<p>\u2714\ufe0f \ud398\uc774\uc9c0\ub124\uc774\uc158 +\ud14c\ub9c8 \/ Pagination + Theme<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"460\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-pagenation-jpg.jpg\" alt=\"\" class=\"wp-image-11948\" srcset=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-pagenation-jpg.jpg 800w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-pagenation-jpg-300x173.jpg 300w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-pagenation-jpg-768x442.jpg 768w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-pagenation-jpg-400x230.jpg 400w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><figcaption class=\"wp-element-caption\">pagenation + theme<\/figcaption><\/figure>\n\n\n\n<p>\u2714\ufe0f\uc88b\uc544\uc694 + \ubd80\uc2a4\ud2b8(\ub85c\uceec) \/ Like + Boost (Local)<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"559\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-localtest-favorite-reblog-jpg-1024x559.jpg\" alt=\"\" class=\"wp-image-11953\" srcset=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-localtest-favorite-reblog-jpg-1024x559.jpg 1024w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-localtest-favorite-reblog-jpg-300x164.jpg 300w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-localtest-favorite-reblog-jpg-768x419.jpg 768w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-localtest-favorite-reblog-jpg-1536x839.jpg 1536w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-localtest-favorite-reblog-jpg-2048x1118.jpg 2048w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-localtest-favorite-reblog-jpg-1870x1021.jpg 1870w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-localtest-favorite-reblog-jpg-400x218.jpg 400w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-localtest-favorite-reblog-jpg-800x437.jpg 800w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Local<\/figcaption><\/figure>\n\n\n\n<p>\u2714\ufe0f\uc88b\uc544\uc694 + \ubd80\uc2a4\ud2b8 \/ Like + Boost(GTS)<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"382\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-gtstest-favourite-reblog.jpg\" alt=\"\" class=\"wp-image-11952\" style=\"width:824px;height:auto\" srcset=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-gtstest-favourite-reblog.jpg 800w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-gtstest-favourite-reblog-300x143.jpg 300w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-gtstest-favourite-reblog-768x367.jpg 768w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/10\/myapp60-gtstest-favourite-reblog-400x191.jpg 400w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><figcaption class=\"wp-element-caption\">GTS<\/figcaption><\/figure>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\ub9c8\ud0dc\ubcf5\uc74c 21\uc7a5 22\uc808 \/ Matthew 21: 22<\/p>\n\n\n\n<p>&#8220;\ub108\ud76c\uac00 \uae30\ub3c4\ud560 \ub54c\uc5d0 \ubb34\uc5c7\uc774\ub4e0\uc9c0 \ubbff\uace0 \uad6c\ud558\ub294 \uac83\uc740 \ub2e4 \ubc1b\uc73c\ub9ac\ub77c \ud558\uc2dc\ub2c8\ub77c&#8221;<\/p>\n\n\n\n<p>&#8220;And everything you ask in prayer, believing, you will receive.&#8221;<\/p>\n<\/blockquote>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\ud83d\udc49\ud83c\udffb myapp60\uc5d0\uc11c\ub294 \ud398\uc774\uc9c0\ub124\uc774\uc158 \uae30\ub2a5 \ucd94\uac00\ub85c \uc778\ud574 \ubc1c\uc0dd\ud55c \uc88b\uc544\uc694,\ubd80\uc2a4\ud2b8 \ubd80\ubd84 \uc624\ub958\ub97c \uc218\uc815\ud569\ub2c8\ub2e4.In myapp60, we are fixing errors related to the &#8220;Like&#8221; and &#8220;Boost&#8221; features that arose from the addition of pagination. \ud83d\udc49\ud83c\udffb \ud398\uc774\uc9c0\ub124\uc774\uc158 \ubd80\ubd84\uc744 \uc644\ub8cc \ud569\ub2c8\ub2e4.I will complete the pagination section. \ud83d\udc49\ud83c\udffb \uc5d1\ud2f0\ube0c\ud14c\ub9c8 \uc790\ub3d9\uc804\ud658\uc774 \ub418\uc9c0 \uc54a\ub294 \ubb38\uc81c\ub97c \uc218\uc815\ud569\ub2c8\ub2e4.Fixes an issue where the active theme does not [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":11948,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[28,27,1,7],"tags":[],"class_list":["post-11888","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-news","category-nextjs","category-uncategorized","category-website","has-thumbnail"],"_links":{"self":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/11888","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/comments?post=11888"}],"version-history":[{"count":57,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/11888\/revisions"}],"predecessor-version":[{"id":11958,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/11888\/revisions\/11958"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/media\/11948"}],"wp:attachment":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/media?parent=11888"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/categories?post=11888"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/tags?post=11888"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}