[インデックス 11269] ファイルの概要
このコミットは、Go言語のドキュメンテーションツールであるgodoc
において、HTMLドキュメントの「Canonical Path(正規パス)」をサポートするための機能追加です。具体的には、HTMLファイル内に埋め込まれたメタデータから正規パスを読み取り、もしユーザーが古いパスや非正規のパスでアクセスした場合に、自動的に正規パスへHTTP 301 (Moved Permanently) リダイレクトを行うようにgodoc
の挙動を変更します。これにより、ドキュメントのURLの一貫性を保ち、検索エンジン最適化(SEO)の観点からも有利になります。
コミット
commit 8bbe5ccb71b7dea0bb814decc80e7a2e53edf07d
Author: Andrew Gerrand <adg@golang.org>
Date: Fri Jan 20 07:37:36 2012 +1100
godoc: support canonical Paths in HTML metadata
Redirect to the canonical path when the old path is accessed.
R=gri
CC=golang-dev
https://golang.org/cl/5536061
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8bbe5ccb71b7dea0bb814decc80e7a2e53edf07d
元コミット内容
godoc: support canonical Paths in HTML metadata
Redirect to the canonical path when the old path is accessed.
R=gri
CC=golang-dev
https://golang.org/cl/5536061
変更の背景
godoc
はGo言語の公式ドキュメントサーバーとしても機能しており、多くのユーザーがこのツールを通じてGoのドキュメントにアクセスします。Webコンテンツにおいて、同じ内容が複数のURLでアクセスできる状態は、いくつかの問題を引き起こす可能性があります。
- 検索エンジン最適化 (SEO) の問題: 検索エンジンは、同じコンテンツが複数のURLに存在すると、それらを重複コンテンツとみなし、どのURLをインデックスすべきか判断に迷うことがあります。これにより、検索ランキングが低下したり、ページが適切にインデックスされない可能性があります。
- URLの一貫性: ユーザーがドキュメントを参照する際に、常に同じURLでアクセスできることは、ブックマークの管理や、他のサイトからのリンクの信頼性を高める上で重要です。
- 分析の正確性: 複数のURLが存在すると、ウェブサイトのアクセス解析を行う際に、どのURLがどれだけアクセスされているかを正確に把握するのが難しくなります。
このコミット以前のgodoc
では、例えば/doc/root.html
という実際のファイルパスと、そのコンテンツが提供されるルートパス/
が異なる場合、両方のURLでコンテンツにアクセスできてしまう状況がありました。この変更は、このような問題を解決し、特定のコンテンツには常に「正規のURL」でアクセスさせることを目的としています。ユーザーが非正規のURLにアクセスした際には、HTTP 301 (Moved Permanently) リダイレクトを返すことで、ブラウザや検索エンジンに正規のURLを伝え、将来的にそのURLを使用するように促します。
前提知識の解説
godoc
とは
godoc
は、Go言語のソースコードからドキュメントを生成し、HTTPサーバーとして提供するツールです。Goのパッケージ、関数、型、変数などのドキュメントを、コメントから自動的に抽出し、ウェブブラウザで閲覧可能な形式で表示します。また、Goの標準ライブラリのドキュメントもgodoc
によって提供されています。
Canonical Path(正規パス)
WebサイトにおけるCanonical Path(正規パス)とは、特定のコンテンツに対する「公式」または「推奨」されるURLのことです。例えば、http://example.com/page
とhttp://example.com/page/index.html
が同じコンテンツを表示する場合、どちらか一方を正規パスとして指定します。これにより、検索エンジンは重複コンテンツと判断せず、指定された正規パスを優先的にインデックスします。
HTTP 301 (Moved Permanently) リダイレクト
HTTPステータスコード301は、「Moved Permanently(恒久的に移動しました)」を意味します。これは、リクエストされたリソースが新しいURLに永続的に移動したことをクライアント(ブラウザや検索エンジンのクローラーなど)に伝えます。クライアントは、この新しいURLを記憶し、将来のすべてのリクエストで新しいURLを使用するべきであると解釈します。SEOの観点からは、301リダイレクトは元のURLの検索エンジンランキングの評価を新しいURLに引き継ぐ効果があるため、非常に重要です。
Go言語の並行処理(GoroutineとChannel)
- Goroutine: Go言語における軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数千、数万のGoroutineを同時に実行できます。
go
キーワードを使って関数を呼び出すことで、新しいGoroutineが起動します。 - Channel: Goroutine間で安全にデータを送受信するための通信メカニズムです。チャネルを通じてデータをやり取りすることで、共有メモリによる競合状態(Race Condition)を避けることができます。
HTMLファイル内のJSON形式メタデータ
このコミットでは、HTMLファイルの先頭に特殊なコメント形式でJSONデータを埋め込むことで、そのHTMLページに関するメタデータ(タイトル、正規パスなど)を定義しています。
例:
<!--{
"Title": "Documentation",
"Path": "/doc/"
}-->
この形式は、HTMLのコメントとして扱われるため、ブラウザには表示されませんが、godoc
のようなツールがパースして利用することができます。
技術的詳細
このコミットの主要な変更点は、godoc
が提供するHTMLドキュメントに正規パスの概念を導入し、それに基づいてリダイレクト処理を行うメカニズムを実装したことです。
-
メタデータ構造体の拡張:
Metadata
構造体にPath
フィールドが追加されました。これは、そのHTMLドキュメントの正規URLパスを保持します。また、内部的にファイルシステム上の相対パスを保持するfilePath
フィールドも追加されています。type Metadata struct { Title string Subtitle string Path string // canonical path for this page filePath string // filesystem path relative to goroot }
-
メタデータ抽出ロジックの改善:
extractMetadata
関数が新しく導入され、HTMLファイルのバイトスライスから先頭のJSON形式メタデータを安全に抽出し、Metadata
構造体にデコードする役割を担います。これにより、メタデータが存在しない場合や、JSONのパースに失敗した場合でも適切に処理されます。 -
メタデータキャッシュの導入:
docMetadata RWValue
という新しいグローバル変数が導入されました。これは、map[string]*Metadata
型の値を安全に読み書きするためのラッパーです。godoc
が提供するすべてのHTMLドキュメントのメタデータがこのマップにキャッシュされます。キーとしては、正規パス(meta.Path
)とファイルシステム上のパス(meta.filePath
)の両方が使用され、どちらのパスでアクセスされても対応するメタデータを迅速に取得できるようにしています。 -
メタデータ更新メカニズム:
updateMetadata
関数が、$GOROOT/doc
ディレクトリ以下を再帰的にスキャンし、すべてのHTMLファイルからメタデータを抽出してdocMetadata
キャッシュを構築します。refreshMetadataSignal
というチャネルが導入され、メタデータの更新をトリガーするためのシグナルを送受信します。refreshMetadataLoop
というGoroutineがバックグラウンドで動作し、refreshMetadataSignal
からのシグナルを受け取るか、または前回の更新から10秒以上経過した場合にupdateMetadata
を呼び出してメタデータを更新します。これにより、ファイルシステム上のドキュメントが変更された場合でも、godoc
が提供するメタデータが最新の状態に保たれます。- 既存の
invalidateIndex
関数(検索インデックスを無効化する関数)からもrefreshMetadata
が呼び出されるようになり、ファイルシステム変更時にメタデータも同時に更新されるようになりました。
-
リダイレクト処理の実装:
serveFile
ハンドラが変更され、リクエストされたURLパスに対応するメタデータが存在するかどうかをmetadataFor
関数を使って確認します。もしメタデータが存在し、かつリクエストされたパス(r.URL.Path
)がメタデータに定義された正規パス(m.Path
)と異なる場合、http.Redirect
関数を使ってHTTP 301リダイレクトを正規パスへ行います。これにより、ユーザーは常に正規のURLに誘導されます。
コアとなるコードの変更箇所
doc/docs.html
:--- a/doc/docs.html +++ b/doc/docs.html @@ -1,5 +1,6 @@ <!--{ - "Title": "Documentation" + "Title": "Documentation", + "Path": "/doc/" }-->
doc/root.html
:--- a/doc/root.html +++ b/doc/root.html @@ -1,3 +1,7 @@ +<!--{ + "Path": "/" +}--> + <link rel="stylesheet" type="text/css" href="/doc/frontpage.css">
src/cmd/godoc/godoc.go
:docMetadata RWValue
の追加。Metadata
構造体の定義変更(Path
とfilePath
フィールドの追加)。serveHTMLDoc
におけるメタデータ抽出ロジックの変更。serveFile
におけるリダイレクトロジックの追加。extractMetadata
関数の新規追加。updateMetadata
関数の新規追加。refreshMetadataSignal
チャネルの新規追加。refreshMetadata
関数の新規追加。refreshMetadataLoop
関数の新規追加。metadataFor
関数の新規追加。invalidateIndex
からのrefreshMetadata
呼び出しの追加。
src/cmd/godoc/main.go
:refreshMetadataLoop()
をGoroutineとして起動する行の追加。
コアとなるコードの解説
Metadata
構造体
type Metadata struct {
Title string
Subtitle string
Path string // canonical path for this page
filePath string // filesystem path relative to goroot
}
Path
フィールドは、このHTMLドキュメントの正規URLパスを定義します。例えば、/doc/
や/
などです。filePath
は、$GOROOT
からの相対的なファイルシステム上のパスを保持し、内部的なマッピングに使用されます。
extractMetadata
関数
func extractMetadata(b []byte) (meta Metadata, tail []byte, err error) {
tail = b
if !bytes.HasPrefix(b, jsonStart) {
return
}
end := bytes.Index(b, jsonEnd)
if end < 0 {
return
}
b = b[len(jsonStart)-1 : end+1] // drop leading <!-- and include trailing }
if err = json.Unmarshal(b, &meta); err != nil {
return
}
tail = tail[end+len(jsonEnd):]
return
}
この関数は、HTMLファイルのバイトスライスb
を受け取り、先頭に埋め込まれたJSON形式のメタデータを抽出します。jsonStart
(<!--{
) と jsonEnd
(}-->
) で囲まれた部分をJSONとしてパースし、Metadata
構造体にデコードします。メタデータ部分を除いた残りのバイトスライスをtail
として返します。
updateMetadata
関数
func updateMetadata() {
metadata := make(map[string]*Metadata)
var scan func(string) // scan is recursive
scan = func(dir string) {
fis, err := fs.ReadDir(dir)
// ... (error handling and directory iteration) ...
for _, fi := range fis {
name := filepath.Join(dir, fi.Name())
if fi.IsDir() {
scan(name) // recurse
continue
}
if !strings.HasSuffix(name, ".html") {
continue
}
// Extract metadata from the file.
b, err := ReadFile(fs, name)
// ... (error handling) ...
meta, _, err := extractMetadata(b)
// ... (error handling) ...
meta.filePath = filepath.Join("/", name[len(*goroot):])
if meta.Path == "" {
// If no Path, canonical path is actual path.
meta.Path = meta.filePath
}
// Store under both paths.
metadata[meta.Path] = &meta
metadata[meta.filePath] = &meta
}
}
scan(filepath.Join(*goroot, "doc"))
docMetadata.set(metadata)
}
この関数は、$GOROOT/doc
ディレクトリ以下を再帰的に走査し、すべてのHTMLファイルを見つけます。各HTMLファイルからextractMetadata
を使ってメタデータを抽出し、Metadata
構造体のfilePath
を設定します。もしPath
フィールドが空の場合、filePath
を正規パスとして使用します。最後に、正規パスとファイルシステム上のパスの両方をキーとして、抽出したMetadata
をdocMetadata
マップに格納します。このマップはRWValue
によって安全に更新されます。
refreshMetadataLoop
とrefreshMetadataSignal
var refreshMetadataSignal = make(chan bool, 1)
func refreshMetadata() {
select {
case refreshMetadataSignal <- true:
default:
}
}
func refreshMetadataLoop() {
for {
<-refreshMetadataSignal
updateMetadata()
time.Sleep(10 * time.Second) // at most once every 10 seconds
}
}
refreshMetadataSignal
はバッファ付きチャネルで、refreshMetadata
が呼び出されるとtrue
を送信します。refreshMetadataLoop
はGoroutineとして起動され、このチャネルからのシグナルを待ち受けます。シグナルを受け取るとupdateMetadata
を呼び出してメタデータを更新し、その後10秒間スリープします。これにより、メタデータの更新が頻繁に行われすぎるのを防ぎつつ、必要に応じて更新がトリガーされるようになります。
serveFile
内のリダイレクトロジック
func serveFile(w http.ResponseWriter, r *http.Request) {
relpath := r.URL.Path
// Check to see if we need to redirect or serve another file.
if m := metadataFor(relpath); m != nil {
if m.Path != relpath {
// Redirect to canonical path.
http.Redirect(w, r, m.Path, http.StatusMovedPermanently)
return
}
// Serve from the actual filesystem path.
relpath = m.filePath
}
relpath = relpath[1:] // strip leading slash
abspath := absolutePath(relpath, *goroot)
// ... (rest of the file serving logic) ...
}
serveFile
関数は、HTTPリクエストのパス(r.URL.Path
)を受け取ります。まず、metadataFor
関数を使って、そのパスに対応するMetadata
が存在するかどうかを確認します。もしメタデータが存在し、かつリクエストされたパスがメタデータに定義された正規パス(m.Path
)と異なる場合、http.Redirect
を呼び出してHTTP 301リダイレクトを正規パスへ行い、処理を終了します。これにより、ユーザーは常に正規のURLに誘導されます。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/8bbe5ccb71b7dea0bb814decc80e7a2e53edf07d
- Gerrit Change-Id: https://golang.org/cl/5536061
参考にした情報源リンク
- Go言語公式ドキュメント: https://go.dev/doc/
- MDN Web Docs - HTTPリダイレクト: https://developer.mozilla.org/ja/docs/Web/HTTP/Redirections
- Google Search Central - 重複コンテンツ: https://developers.google.com/search/docs/fundamentals/seo-starter-guide/on-page-basics?hl=ja#duplicate-content