# -----------------------------------------------------------------------------
# Tripletail::Filter::SEO - SEO出力フィルタ
# -----------------------------------------------------------------------------
package Tripletail::Filter::SEO;
use strict;
use warnings;
use Tripletail;
require Tripletail::Filter;
our @ISA = qw(Tripletail::Filter);
# このフィルタはリンクのQUERY_STRINGを次のようにPATH_INFOに変換する。
#
#
# ↓
#
#
# クエリの中にキーワードSEO=1を含んでいるもののみを対象とし、
# リンク変換後にはSEO=1は消去する。
#
#
# また、head要素内にbase要素を追加する。
# head要素が存在しない場合はbody要素開始直前にhead要素が挿入されるが、
# body要素も存在しなければ何も挿入されない。
# 元々base要素が存在した場合はそのhref属性が置き換えられる。
#
# REQUEST_URI: http://foo.com/bar/baz.cgi
# 挿入される要素:
# オプション一覧:
# * hide_extension => リンク変換の際、拡張子を削除するかどうか。デフォルト1。
# 注意:
# * このフィルタはTripletail::Filter::HTMLやTripletail::Filter::MobileHTMLよりも
# 後に実行されるように設定しなければならない。
# * Tripletail::Filter::HTML, Tripletail::Filter::MobileHTML がタグの途中で区切って
# 出力を行わない性質に依存している。
# それ以外のカスタムフィルタと兼用する場合はその点を注意する
# 必要がある。
# * JISコードでの出力時は正常に動作しない。
# * charsetにはTripletail::Filter::HTMLまたはTripletail::Filter::MobileHTMLと
# 同じ文字コードを指定しなければならない。
1;
sub _new {
my $class = shift;
my $this = $class->SUPER::_new(@_);
my $defaults = [
[charset => 'Shift_JIS'],
[hide_extension => 1],
];
$this->_fill_option_defaults($defaults);
my $check = {
charset => [qw(defined no_empty scalar)],
hide_extension => [qw(scalar)],
};
$this->_check_options($check);
$this->reset;
$this;
}
sub print {
my $this = shift;
my $str = shift;
if(ref($str)) {
die __PACKAGE__."#print: arg[1] is a reference. [$str] (第1引数がリファレンスです)\n";
}
$this->_filter($str);
}
sub flush {
my $this = shift;
'';
}
sub setOrder {
my $this = shift;
my @order = @_;
my $count = 0;
foreach my $data (@order) {
$count++;
if(ref($data)) {
die __PACKAGE__."#setOrder: arg[$count] is a reference. [$data] (第${count}引数がリファレンスです)\n";
}
}
$this->{order} = \@order;
$this;
}
sub toLink {
my $this = shift;
my $form = shift;
my $base = shift;
if(!defined($form)) {
die __PACKAGE__."#toLink: arg[1] is not defined. (第1引数が指定されていません)\n";
} elsif(ref($form) ne 'Tripletail::Form') {
die __PACKAGE__."#toLink: arg[1] is not an instance of Tripletail::Form. [$form]. (第1引数がFormオブジェクトではありません)\n";
}
if(ref($base)) {
die __PACKAGE__."#toLink: arg[2] is a reference. (第2引数がリファレンスです)\n";
}
$form = $form->clone->delete('SEO');
my $link = $form->toLink($base);
my $query = $TL->newForm($TL->unescapeTag($link));
$this->__makeLink($link, $query);
}
sub reset {
my $this = shift;
$this->SUPER::reset();
$this->{header_skipped} = undef; # HTTPヘッダ部を通過した後であれば1
$this->{rebased} = undef; # base要素を書換え、若しくは追加した後であれば1
$this->{order} = []; # 出力順の指定
$this;
}
sub _filter {
my $this = shift;
my $str = shift;
my $header = '';
if(!$this->{header_skipped}) {
# HTTPヘッダ部を通過するまで何もしない。
while($str =~ s/^(.+?\r?\n)//) {
$header .= $1;
if($1 !~ m/\S/) {
# ヘッダ終了
$this->{header_skipped} = 1;
last;
}
}
}
if(length($str)) {
$str = $this->_rebase($str);
$str = $this->_relink($str);
}
$header . $str;
}
sub _rebase {
my $this = shift;
my $html = shift;
if($this->{rebased}) {
return $html;
}
my $filter = $TL->newHtmlFilter(
interest => [qw[/head base body]],
);
$filter->set($html);
my $baseurl = $ENV{REQUEST_URI} || '';
$baseurl =~ s![^/]+$!!;
if(!length($baseurl)) {
# REQUEST_URIが与えられていないので何も出来ない。
return $html;
}
$baseurl = sprintf(
'%s://%s%s',
($ENV{HTTPS} and $ENV{HTTPS} eq 'on') ? 'https' : 'http',
$ENV{SERVER_NAME},
$baseurl,
);
while(my ($context, $elem) = $filter->next) {
if($this->{rebased}) {
next;
}
if($elem->name eq 'base') {
my $target = $elem->attr('target');
if(!defined($target) || $target eq '_self') {
$elem->attr(href => $baseurl);
$this->{rebased} = 1;
#$TL->log(
# __PACKAGE__,
# sprintf(
# 'Modified existent base element: [%s] => [%s]',
# $elem->attr('href'), $baseurl)
#);
}
} elsif($elem->name eq '/head') {
# 直前に挿入。
my $base = $context->newElement('base');
$base->attr(href => $baseurl);
$context->delete;
$context->add($base);
$context->add($elem);
$this->{rebased} = 1;
#$TL->log(
# __PACKAGE__,
# sprintf(
# 'Inserted base element before the end of head element: [%s]',
# $baseurl)
#);
} elsif($elem->name eq 'body') {
# 直前にheadを挿入。
$context->delete;
$context->add($context->newElement('head'));
my $base = $context->newElement('base');
$base->attr(href => $baseurl);
$context->add($base);
$context->add($context->newElement('/head'));
$this->{rebased} = 1;
#$TL->log(
# __PACKAGE__,
# sprintf(
# 'Inserted head and base elements before the beginning of body element: [%s]',
# $baseurl)
#);
}
}
$filter->toStr;
}
sub _relink {
my $this = shift;
my $html = shift;
my $filter = $TL->newHtmlFilter(
interest => ['a', 'form'],
);
$filter->set($html);
while(my ($context, $elem) = $filter->next) {
if($elem->name eq 'a') {
my $link = $elem->attr('href');
defined $link or next;
my $query = $TL->newForm($TL->unescapeTag($link));
if($query->get('SEO')) {
# 対象になっている
$query->delete('SEO');
my $new_url = $this->__makeLink($link, $query);
$elem->attr(href => $new_url)
}
} elsif($elem->name eq 'form') {
$elem->attr(EXT => undef);
}
}
$filter->toStr;
}
sub __makeLink {
my $this = shift;
my $link = shift;
my $query = shift;
local($_);
my @params;
foreach my $key (@{$this->{order}}) {
if($query->exists($key)) {
foreach my $value (sort $query->getValues($key)) {
push(@params, $key, $value);
}
$query->delete($key);
}
}
foreach my $key (sort $query->getKeys) {
next if($key eq 'INT');
foreach my $value (sort $query->getValues($key)) {
push(@params, $key, $value);
}
}
my $path_info = join(
'/', map {
$TL->encodeURL($_);
} @params);
if(defined($_ = $query->getFragment)) {
$path_info .= '#' . $TL->encodeURL($_);
}
(my $file = $link) =~ s/\?.*$//;
if($this->{option}{hide_extension}) {
$file =~ s/\..+$//; # 拡張子を消す
}
my $new_url = $file . ($path_info ? "/$path_info" : '');
#$TL->log(__PACKAGE__, sprintf ('Relinked: [%s] => [%s]', $elem->attr('href'), $new_url));
$new_url;
}
__END__
=encoding utf-8
=head1 NAME
Tripletail::Filter::SEO - SEO出力フィルタ
=head1 SYNOPSIS
$TL->setContentFilter('Tripletail::Filter::HTML');
$TL->setContentFilter(['Tripletail::Filter::SEO', 1001]);
$TL->print($TL->readTextFile('foo.html'));
=head1 DESCRIPTION
このフィルタはリンクの C を次のように C に変換する。
↓
クエリの中にキーワードSEO=1を含んでいるもののみを対象とし、
リンク変換後にはSEO=1は消去する。
また、head要素内にbase要素を追加する。head要素が存在しない場合は
body要素開始直前にhead要素が挿入されるが、body要素も存在しなければ
何も挿入されない。元々base要素が存在した場合はそのhref属性が置き換えられる。
REQUEST_URI: http://foo.com/bar/baz.cgi
挿入される要素:
注意:
このフィルタは L や L
よりも後に実行されるように設定しなければならない。
また、リンクを書き換えた場合、そのリクエストは L
を使用しなければ正常に受け取れない。
出力は Shift_JIS,EUC-JP,UTF-8 のいずれかでなければならない。
JIS コードの場合は正常に動作しない。
=head2 METHODS
=over 4
=item setOrder
$TL->getContentFilter(1001)->setOrder(qw(ID Name));
SEO変換時に、出力するキーの順序を指定します。
指定されていないキーは、setOrder で指定されたキーの後に文字列順にソートされて出力されます。
=item toLink
$TL->getContentFilter(1001)->toLink($TL->newForm(KEY => 'VALUE'));
フォームオブジェクトを、SEO変換と同様の形でリンクに変換します。
=item flush
L参照
=item print
L参照
=item reset
L参照
=back
=head2 フィルタパラメータ
=over 4
=item hide_extension
$TL->setContentFilter(['Tripletail::Filter::SEO', 1001], hide_extension => 0);
リンク変換の際、拡張子を削除するかどうか。省略可能。デフォルトは1。
=back
=head1 SEE ALSO
=over 4
=item L
=item L
=item L
=item L
=back
=head1 AUTHOR INFORMATION
=over 4
Copyright 2006 YMIRLINK Inc.
This framework is free software; you can redistribute it and/or modify it under the same terms as Perl itself
このフレームワークはフリーソフトウェアです。あなたは Perl と同じライセンスの 元で再配布及び変更を行うことが出来ます。
Address bug reports and comments to: tl@tripletail.jp
HP : http://tripletail.jp/
=back
=cut