2026年8月17日 星期一

For next.js:build時好好的,上線卻遇到package沒裝的錯誤

 最近在幫一個服務四個國家的技訓機構寫外場app(教師&學員各有不同的頁面) .裡面會有製表呈現單元.

在現場執行時,下載pdf的時候出現這樣的錯誤
-------------------------

{"level":"error","message":"attendance PDF error: Could not find Chrome (ver. 148.0.7778.97).
 This can occur if either
 1. you did not perform an installation before running the script  (e.g. npx puppeteer browsers install chrome) or
 2. your cache path is incorrectly configured 
 (which is: C:\\Users\\Administrator\\.cache\\puppeteer).
 For (2), check out our guide on configuring puppeteer 
 at https://pptr.dev/guides/configuration.","timestamp":"......." }

-------------------------

「可是明明有做npm install了啊,怎麼會有這現象」

「npm install有時會有下載失敗的錯誤只是我們沒看到」
這時要手動強制下載安裝了,沒那麼可怕,只是需要一行指令罷了

--------------------
npx puppeteer browsers install chrome
--------------------

「可是這樣只會安裝在我的目錄(C:\\Users\\MyAccount\\.cache\\puppeteer)中,log說是要放在Administrator中,如何突破這個限制?」

你問得好,接下來這招不錯哦--

1.安裝套件指定位置並安裝之:
-----------------------------
set PUPPETEER_CACHE_DIR=D:\puppeteer-cache
npx puppeteer browsers install chrome
--------------------

2.在ecosystem.config.js設定它 (當然要先切換到你git pull出來& build出來的目錄裡)
-------------------
 PUPPETEER_CACHE_DIR: 'D:\\puppeteer-cache'
--------------------

3.別忘了pm2需重啟載入執行
--------------------
pm2 delete xxxxx(你的application name)
pm2 restart ecosystem.config.js
pm2 save
--------------------
確認pm2 有把你的app執行起來就可以了

「那....我想再問一下,怎麼知道設定有吃進去」

如何驗證上述的「PUPPETEER_CACHE_DIR」有載入,只要一個執令

--------------
pm2 env 你的app id number (1、2、3.....)
--------------
這時你的console就會列出pm2所載入的資料
----------------


--------------------------------------------


以上,一起加油吧







2026年8月10日 星期一

for nex.js :什麼時候該「rm -rf .next」

 有時明明之前build的結果執行很正確
但改了設定就會出現404錯誤
這時就是砍快取的時候了
是什麼時候會用到這招(rm -rf .next) ? :

1.新建的路由檔案即使路徑正確也出現 404 錯誤——像是修改 .env.local設定中的 「NEXT_PUBLIC_BASE_PATH」 後,Turbopack 的路由清單有時會遺漏新建立的嵌套動態資料夾。

2.更改後next.config.tsbasePath. output、、和serverExternalPackages都會影響構建樹的佈局方式,並且舊配置的過時產物會殘留下來。

3.在移動或重新命名項目資料夾之後—.next/standalone它包含已解析的路徑。

總之,當症狀與你正在閱讀的程式碼完全不符時,如果檔案明顯正確但行為不符,那麼在懷疑自己之前,首先應該懷疑快取問題。

以上,一起加油吧!! 


2026年7月8日 星期三

for next.js:什麼時候用req:Request ,什麼時候用req:NextRequest ?

 不仔細想的話,直接全部都用「req:NextRequest」,
因為 NextRequest extends Request

說明:
-------------------

// NextRequest EXTENDS Request:
// = Request + extra Next.js features
req.nextUrl          // ← parsed URL object (convenient!)
req.nextUrl.searchParams  // ← query params directly
req.nextUrl.pathname      // ← /api/books
req.geo              // ← geo location (Edge only)
req.ip               // ← client IP
req.cookies          // ← cookies helper
  .get('name')
  .set('name', value)
  .delete('name')

-------------------
用Request的時機
-------------

//有query string傳值的時候
export async function GET(req: Request) {

  const url = new URL(req.url)

  const param = url.searchParams.get('keyword')

  // ← manual URL parsing needed

}


export async function POST(req: Request) {

  const body = await req.json()  // ← same on both

  ...

}

export async function POST(req: Request) {

  const { userId, password } = await req.json()

}

-------------
用NextRequest的時候--有query string傳參的時間
--------------

export async function GET(req: NextRequest) {

  const keyword = req.nextUrl.searchParams.get('keyword')

}


export async function POST(req: NextRequest) {

  const body = await req.json()  // ← works perfectly ✓

  const { title, author } = body

}

--------------
以上

2026年6月22日 星期一

為什麼在寫Next.js route中,參數要寫做「{params}....」而不是直接寫做params,背後是有什麼機制?

 如果你有在寫Next.js的api route.ts的話,會常看到類似以下的片段
------------

export async function GET(

  req: Request,

  {params}: { params: Promise<{ batchId: string }> }

) {

  const { batchId } = await params
...
....
...

}

------------
為什麼是寫作「{params}」而非是「params」就好?
事實上,那是簡寫,真正的code是
----
export async function GET(
  req: Request,
  context: { params: Promise<{ batchId: string }> }
) {
  const { batchId } = await context.params
...
...
}

----
那問題來了,context是什麼,route到底會收到什麼參數?
routing從url的path解析出[...]/[...]/ 對應到相對的route.ts及方法後(以GET為例),參數內容如下
-------------
GET(req, context)
  │         │
  │         └── Route context (URL structure)
  │               context.params  → path segments [batchId],
          
                                               [sessionId]
  │               context.searchParams →
  │               query string (?tab=x) [page routes only]
  │
  └── Request object (HTTP request details)
req.method   → GET / POST / DELETE
req.headers  → Authorization, Content-Type, cookies...
req.body     → POST/PUT body (JSON payload)
req.url      → full URL string
-------------
就是這樣的內容,AI會直接把context......寫成{params} 的簡寫方式
A:「那,如果後面帶『?key=value』的方式如何取值?」
Q:就要從req.url轉換出「searchParams」了
--------------
  const { searchParams } = new URL(req.url);

  // Example:
  // /api/user?id=1&name=john&role=admin
  const id = searchParams.get("id");
  const name = searchParams.get("name");
  const role = searchParams.get("role");

上述是在route.ts裡的寫法

而在tsx裡的寫法卻又是不一樣了

-----如同之前context.searchParams → query string (?tab=x) [page routes only]所示-------
// Page components (page.tsx) ONLY:
export default function Page({
  params,
  searchParams,  // ← available here as context prop
}: {
  params: Promise<{ batchId: string }>
  searchParams: Promise<{ tab: string }>
}) {
  const { batchId } = use(params)
  const { tab } = use(searchParams)  // ← works in page.tsx
   ...
   .....
   ...
}
------------
 ts & tsx 寫法不同,千萬要注意
(好像對AI generated code如此講究的工程師也不多了吧.現在都是直接測了能跑交件了)




2026年5月19日 星期二

for next.js 16,開發環境中:為什麼訪問api可以,但訪問「/」就整台機器給你掛掉

為了怕日後又重複遇到這個現象,所以做這筆記... 

在學習專案中先以一個route.ts測試db部分
------src/app/api/test/route.ts------------

import db from '@/lib/db';
import { NextResponse } from 'next/server';
export async function GET(){
    try{
         const result=await db('books').count(' * as total').first();
         return NextResponse.json({
                success:true,
                totalBooks:result?.total
            });
    }catch(error){
            return NextResponse.json({
                    success:false,
                    error:String(error)
                },{
                    status:500
                });
    }
}

--------------------------------------------
在terminal用"npm run dev" 起來要查看個api  (url : http://localhost3000/api/test)時
系統會出現一堆package沒安裝的訊息,這時,別傻傻地一個一個安裝(因為knex 依賴的套件如果沒有特別指定.會是所有的db都來一份的)
此時請到「next.config.ts」設定,因為我們這次用了knex操作postgresql,所以在serverExternalPackages中,加上「knex、pg」套件
--------------

import type { NextConfig } from "next";
const nextConfig: NextConfig = {
  /* config options here */
 serverExternalPackages: ['knex', 'pg', 'jsonwebtoken', 'bcryptjs'],
};
export default nextConfig;

---------------

總該可以測了吧,是的,你可以看到api如期工作








現在,我們要回去幹正事了--回頭查看一下localhot:3000/
結果terminal一直在compiling....






然後系統就整個給你關機罷工去了!!! (連個error都不response.....)
雖然說「佛跳牆弄到爆炸是不允許的,應該要踢你出局,不過念在你有創意夠噱頭,還有2 分鐘再做一個吧!」,架構再怎麼好,會雄雄讓人怎麼死的都不知道是不行的

為什麼會這樣!?
因為最初那個test的route.ts比較小,一下子就compile  (webpack)完成可用了
但是request「/」時,"如果沒有指定的話"是整個網站都compile
「那...我只想要compile需要的頁面的話,要如何指定?」
請到「package.json」做以下的修改
----





----

即,把script--> "dev":"next dev "改為"dev":"next dev --turbo"
存檔,就可以解決這個問題了

一起加油吧

2026年5月4日 星期一

沒事把node.js簽發的jwt拿來呼叫.net core web API時踩了一堆坑的故事

 照理說dot net 的 web api與nextjs整合應該是前端用Browser -->next.js --> .net web api (讓dot net的microservice放在另一個網段)
不過如果直接讓node.js簽發給browser client的jwt拿來call .net web api會發生什麼事 (為什麼要這樣做?不為什麼,就只是想知道罷了)

這一路走來我遇到了狀況,因為很有趣,所以把它做個記錄.

狀況一.401錯誤--起源是錯用了AOT,而不是傳統的Web API專案
原因: VS 2026 建專案時選到了 "Web API (Native AOT)" 模板,使用 CreateSlimBuilder + PublishAot。AOT 的 trimmer 會把 JWT 需要的 crypto provider 砍掉。



所以最初在設計Web API時,一定要選對專案型態 ,不是AOT,而是最上面那種傳統Web API型態的專案
AOT與一般web API專案的差別:

                    Regular              AOT
─────────────────────────────────────────────────
Builder           CreateBuilder        CreateSlimBuilder
編譯方式           IL → JIT             直接編譯成機器碼
啟動速度           ~500ms+              ~50ms
發布大小           需要 Runtime          單一檔案 (~10MB)
Reflection         ✅ 完整支援           ❌ 受限
JSON 序列化        System.Text.Json      需要 Source Generator
JWT/Crypto         ✅ 全部可用           ⚠️ 可能被 Trim 掉
NuGet 相容性       幾乎全部              部分不支援
適用場景           一般 API、企業應用     微服務、Serverless

-----------------------------------------------------------------------
(小的沒在用AOT,如有先進有補充建議的亦請不吝賜教)

重新增加一個web api project,修改program.cs
-----------------------------

...
...
var
key = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(Configuration["JWT_SECRET"]) );
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = key, ValidateIssuer = false, ValidateAudience = false, }; });
....
...
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
-----------------------------
Action in Sample controller
-------------------------
[Authorize]
[HttpGet]
public IActionResult Get() {
    var username = User.FindFirst("username")?.Value;
    var role = User.FindFirst("role")?.Value;
    return Ok();
}
----------------------------
之後....
狀況二.錯誤訊息 IDX10517: Signature validation failed. The token's kid is missing.
------------------------------------------
原因: .NET 10 的 Microsoft.IdentityModel 套件收緊了驗證規則,要求 token header 中必須有 kid(Key ID)。而 Node.js 的 jose 預設不產生 kid
解法:
這要在nextjs 簽發JWT及解析端(.net core web API)都要再加一個kid
------JWT.ts-----------
export async function signJWT(user:AuthUser):Promise<string> {
    return new SignJWT(
       {
        id:user.id,
        username:user.username,
        role:user.role
       }
    ).setProtectedHeader({alg:'HS256') <--Before
    ).setProtectedHeader({alg:'HS256', kid: 'MyKidKey'}) <--after.
    .setIssuedAt()
    .setExpirationTime('8h')
    .sign(SECRET); <---SECRET是從.node 設定檔來的,跟program.cs的 Configuration["JWT_SECRET"]相呼應
}
------Program.cs------
var key = new SymmetricSecurityKey(
    Encoding.UTF8.GetBytes(Configuration["JWT_SECRET"])
);
key.KeyId = "MyKidKey";  <----KID set here.
--------------------------
這下子,總該可以了吧?
再次用postman設定好bearer token後,試著request controller 時
出現新的錯誤錯誤--
狀況三:「The signature is invalid」錯誤
這東西真的是超難解的,最後是把兩邊的code都送給AI去看的結果才知是「長度」問題:

原因: HS256 要求 key 至少 256 bits(32 bytes)。原本的 secret 只有 27 字元,jose 和 .NET 對不足長度的 key 可能有不同的 padding 行為。

解法: Secret 改成 32 字元以上,兩邊同步更新。

最後,終於在controller的GET action中,可以讀取到User資訊了
但是「User.FindFirst("role")?.Value」回傳null,這樣還是不完成,
改成「User.FindFirst(ClaimTypes.Role)?.Value」,就會得到role值了

別忘了,在[Authorize...]指定需要的角色權限.ex  [Authorize(Roles = "manager,admin")]

有些知識真的是靠踩坑得來的,不是AI直接給你就會了解的.