URI 是 Web 應用的命脈。Grails 預設 /controller/action/id 雖唯一但缺乏語意。本文示範如何透過 Controller 邏輯、UrlMappings、自訂 Codec 打造可讀、可記、有利 SEO 的友善 URI。
預設 URI:https://localhost:9090/blogito/entry/show/1
/blog/jsmith/grails-codecsclass User {
static constraints = {
login(unique: true)
password(password: true)
name()
}
static hasMany = [entries: Entry]
String login, password, name
String toString() { name }
}
class Entry {
static belongsTo = [author: User]
String title, summary
Date dateCreated, lastUpdated
}
BootStrap.groovy 建立種子資料:
def jdoe = new User(login: 'jdoe', password: 'password', name: 'John Doe')
def e1 = new Entry(title: 'Grails 1.1 beta is out', summary: 'Check out the new features')
jdoe.addToEntries(e1); jdoe.save()

class EntryController {
def list = {
if (!params.max) params.max = 10
flash.id = params.id
if (!params.id) params.id = "No User Supplied"
def author = User.findByLogin(params.id)
def entryList, entryCount
if (author) {
def query = { eq('author', author) }
entryList = Entry.createCriteria().list(params, query)
entryCount = Entry.createCriteria().count(query)
} else {
entryList = Entry.list(params)
entryCount = Entry.count()
}
[entryInstanceList: entryList, entryCount: entryCount]
}
}
關鍵點:
params.id 對應 URL 最後一段(預設映射行為)User.findByLogin 而非 getflash.id 傳遞分頁參數<g:paginate total="${entryCount}" params="${flash}"/>


class UrlMappings {
static mappings = {
"/$controller/$action?/$id?" { constraints {} }
"/"(controller: "entry")
"/blog/$id"(controller: "entry", action: "list")
"/blog/$id/$title?"(controller: "entry", action: "list")
"/entry/$action?/$id?/$title?"(controller: "entry")
"500"(view: '/error')
}
}
現在支援:
/blog/jsmith → 作者列表/blog/jsmith/grails-codecs → 標題模糊比對class UnderscoreCodec {
static encode = { target -> target.replaceAll(" ", "_") }
static decode = { target -> target.replaceAll("_", " ") }
}
所有 String 自動獲得 encodeAsUnderscore()、decodeUnderscore()。
class UnderscoreCodecTests extends GroovyTestCase {
void testEncode() { assertEquals "this_is_a_test", "this is a test".encodeAsUnderscore() }
void testDecode() { assertEquals "this is a test", "this_is_a_test".decodeUnderscore() }
}

def list = {
// ...
flash.title = params.title
if (!params.title) params.title = ""
def author = User.findByLogin(params.id)
if (author) {
def query = {
and {
eq('author', author)
like("title", params.title.decodeUnderscore() + '%')
}
}
entryList = Entry.createCriteria().list(params, query)
entryCount = Entry.createCriteria().count(query)
}
// ...
}
使用 like 支援前綴比對,decodeUnderscore() 還原標題關鍵字。
HtmlCodec:encodeAsHTML()/decodeHTML() 防 XSSURLCodec:encodeAsURL() 查詢字串編碼Base64Codec、JavaScriptCodec置於 grails-app/utils/*Codec.groovy 即自動註冊。
友善 URI 三步走:Controller 語意查詢、UrlMappings 路由重寫、Codec 格式轉換。Blogito 現具備語意化永久連結,下一篇將實作登入驗證與檔案上傳,讓使用者發布富媒體部落格內容。