Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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でアクセスできる状態は、いくつかの問題を引き起こす可能性があります。

  1. 検索エンジン最適化 (SEO) の問題: 検索エンジンは、同じコンテンツが複数のURLに存在すると、それらを重複コンテンツとみなし、どのURLをインデックスすべきか判断に迷うことがあります。これにより、検索ランキングが低下したり、ページが適切にインデックスされない可能性があります。
  2. URLの一貫性: ユーザーがドキュメントを参照する際に、常に同じURLでアクセスできることは、ブックマークの管理や、他のサイトからのリンクの信頼性を高める上で重要です。
  3. 分析の正確性: 複数の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/pagehttp://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ドキュメントに正規パスの概念を導入し、それに基づいてリダイレクト処理を行うメカニズムを実装したことです。

  1. メタデータ構造体の拡張: 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
    }
    
  2. メタデータ抽出ロジックの改善: extractMetadata関数が新しく導入され、HTMLファイルのバイトスライスから先頭のJSON形式メタデータを安全に抽出し、Metadata構造体にデコードする役割を担います。これにより、メタデータが存在しない場合や、JSONのパースに失敗した場合でも適切に処理されます。

  3. メタデータキャッシュの導入: docMetadata RWValueという新しいグローバル変数が導入されました。これは、map[string]*Metadata型の値を安全に読み書きするためのラッパーです。godocが提供するすべてのHTMLドキュメントのメタデータがこのマップにキャッシュされます。キーとしては、正規パス(meta.Path)とファイルシステム上のパス(meta.filePath)の両方が使用され、どちらのパスでアクセスされても対応するメタデータを迅速に取得できるようにしています。

  4. メタデータ更新メカニズム:

    • updateMetadata関数が、$GOROOT/docディレクトリ以下を再帰的にスキャンし、すべてのHTMLファイルからメタデータを抽出してdocMetadataキャッシュを構築します。
    • refreshMetadataSignalというチャネルが導入され、メタデータの更新をトリガーするためのシグナルを送受信します。
    • refreshMetadataLoopというGoroutineがバックグラウンドで動作し、refreshMetadataSignalからのシグナルを受け取るか、または前回の更新から10秒以上経過した場合にupdateMetadataを呼び出してメタデータを更新します。これにより、ファイルシステム上のドキュメントが変更された場合でも、godocが提供するメタデータが最新の状態に保たれます。
    • 既存のinvalidateIndex関数(検索インデックスを無効化する関数)からもrefreshMetadataが呼び出されるようになり、ファイルシステム変更時にメタデータも同時に更新されるようになりました。
  5. リダイレクト処理の実装: 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構造体の定義変更(PathfilePathフィールドの追加)。
    • 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を正規パスとして使用します。最後に、正規パスとファイルシステム上のパスの両方をキーとして、抽出したMetadatadocMetadataマップに格納します。このマップはRWValueによって安全に更新されます。

refreshMetadataLooprefreshMetadataSignal

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