2013/01/08
Confluenceをカスタマイズして会社ブログを作ってみた大貫 浩Hiroshi Ohnuki

お正月休みを使って、Confluenceをカスタマイズして会社ブログ、Ricksoftブログ(このサイト)を作りました。この記事ではどのようにConfluenceをカスタマイズし、このブログを作ったのかを説明します。
このRicksoftブログ作成までの過程を報告します。まず、ブログ開発で目指した目標は以下の3点です。
作業は上記設定した目標の順番に行いました。と、そのまえに会社ブログに合うようにセキュリティ設定を行います。
Confluenceは1インスタンスで複数のスペース(情報の入れ物)を持てます。そしてその単位にセキュリティ設定(権限設定)ができます。まずブログ用スペースを会社ブログに合うようにセキュリティ設定します。設定は簡単でスペース管理の権限で、ログインユーザにブログ作成権限等を与え、匿名ユーザーには表示とコメント追加権限を与えます。設定画面は以下のようになります。
まず手をいれるのは「スペースの管理」のルックアンドフィールです。ここでテーマを「グローバルなルック アンド フィール」にします。これによりHTMLレベルでのカスタマイズが可能になります。
で、実際にHTMLレベルでカスタマイズする際は上記メニューの「レイアウト」からカスタマイズしたい部分を選びます。いろいろな部分をカスタマイズできますが、ほとんどのカスタマイズは「メイン レイアウト」で事足りるはずです。
今回カスタマイズした「メイン レイアウト」は以下の通り。このファイルを読み解くポイントは、以下の点になります。
そして注意しなければいけないのは、カッコ()の数がズレると、ページ描画に失敗して、復旧が困難になります。なぜなら、このスペース管理画面も「メイン レイアウト」を使って描画されているからです。なので、このメインレイアウトを編集する作業は必ずテスト環境で行ってください。自分は今まで生きてきて1回も失敗したことがないという人は別ですが…
メインレイアウト
<!DOCTYPE html>
<html>
<head>
#if ($sitemeshPage.getProperty("page.spacename"))
<title>$title - $sitemeshPage.getProperty("page.spacename") - #siteTitle()</title>
#else
<title>$title - #siteTitle()</title>
#end
#requireResource("confluence.web.resources:print-styles")
#requireResourcesForContext("main")
#requireResourcesForContext("atl.general")
#parse("/decorators/includes/header.vm")
$!settingsManager.globalSettings.customHtmlSettings.beforeHeadEnd
$!sitemeshPage.getProperty("page.canonical")
</head>
## HTML HEADER ENDS
## HTML BODY BEGINS
<body #onLoadAttr() id="com-atlassian-confluence" class="$!theme.bodyClass $!sitemeshPage.getProperty("page.bodyClass")">
<div id="fb-root"></div>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/ja_JP/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>
#parse ("/decorators/includes/main-content-includes.vm")
<ul id="assistive-skip-links" class="assistive">
<li><a href="#title-heading">$action.getText("assistive.skiplink.to.content")</a></li>
<li><a href="\#breadcrumbs">$action.getText("assistive.skiplink.to.breadcrumbs")</a></li>
<li><a href="#header-menu-bar">$action.getText("assistive.skiplink.to.header.menu")</a></li>
<li><a href="#navigation">$action.getText("assistive.skiplink.to.action.menu")</a></li>
<li><a href="#quick-search-query">$action.getText("assistive.skiplink.to.quick.search")</a></li>
</ul>
<div id="page">
<div id="full-height-container">
#if($sitemeshPage.getProperty("page.tree"))
#set($sidebarSettings = $studioSidebarHelper.getSettings($spaceKey))
<div id="splitter">
<div id="splitter-sidebar">
$!sitemeshPage.getProperty("page.theme-navigation")
#if ($!sidebarSettings.isTreeEnabled() == "true")
$!sitemeshPage.getProperty("page.tree")
#end
</div>
<div id="splitter-content">
## script needs to be executed here to prevent jerky content
#includePluginJavascript("com.atlassian.confluence.plugins.doctheme:resources", "doc-theme.js")
#if ($!sitemeshPage.getProperty("page.theme-header"))
$!sitemeshPage.getProperty("page.theme-header")
#end
#end
<!-- \#header -->
<div id="header_rsblog" style="height: 56px; background-color: #EEE; border-bottom: solid 2px #326CA6; margin-bottom: 10px;">
<div id="header_rsblog_inside" style="width: 960px; margin: 15px auto 2px;">
<div style="float:right; ">
<input type="hidden" name="spaceSearch" value="true" />
#customQuickSearch("blog" true true [{"name":"where", "value":"BLOG"}])
## #quickSearch("space=BLOG")
</div>
#logoBlock($spaceKey)
</div>
</div>
## CONTENT DIV BEGINS
<div id="main" class="$!personalClass" style="width: 960px; margin: 0 auto;">
<div id="sidebar-container" style="float:right; ">
#if($showPersonalSidebar)
#if ($sitemeshPage.getProperty("page.personal-sidebar"))
#skiplink("sidebar" $i18n.getText("assistive.skiplink.to.sidebar.start") $i18n.getText("assistive.skiplink.to.sidebar.end"))
$sitemeshPage.getProperty("page.personal-sidebar")
#end
#end
#else
#if ($sitemeshPage.getProperty("page.blog-sidebar"))
#skiplink("sidebar" $i18n.getText("assistive.skiplink.to.sidebar.start") $i18n.getText("assistive.skiplink.to.sidebar.end"))
<div id="blog-sidebar" class="sidebar" >
$!sitemeshPage.getProperty("page.blog-sidebar")
</div><!-- \#blog-sidebar -->
#end
#end
#if ($sitemeshPage.getProperty("page.sidebar"))
#skiplink("sidebar" $i18n.getText("assistive.skiplink.to.sidebar.start") $i18n.getText("assistive.skiplink.to.sidebar.end"))
<div id="sidebar">
$!sitemeshPage.getProperty("page.sidebar")
</div><!-- \#sidebar -->
#end
#end
#end
</div><!-- \#sidebar-container -->
<div id="main-header">
$!sitemeshPage.getProperty("page.content-navigation")
$!sitemeshPage.getProperty("global.dashboard-navigation")
#if ($sitemeshPage.getProperty("page.surtitle"))
#set($creatorName = $sitemeshPage.getProperty("page.surtitle"))
#set($creatorName = $stringUtils.substringAfter($creatorName , "name"))
#set($creatorName = $stringUtils.substringBetween($creatorName , '"', '"'))
#set ($tildeUsername= "~$creatorName")
#end
#if ($sitemeshPage.getProperty("page.postingDay"))
<h1 id="title-heading" class="pagetitle" style="padding-bottom: 10px; margin-bottom: 10px; border-bottom: 2px solid #196DB4;">
<span id="title-text">
$title
</span>
</h1>
<div style="float:left; margin-right: 15px;">
#logoBlock($tildeUsername)
</div>
<div style="display: inline; margin:0 30px 0 0;">
$sitemeshPage.getProperty("page.surtitle")
</div>
<ul style="margin: 5px; list-style-type: none;">
<li style="display: inline;">
<div class="fb-like" data-send="false" data-layout="button_count" data-width="450" data-show-faces="false"></div>
</li>
<li style="display: inline; position: relative;top: 3px;">
<a href="https://twitter.com/share" class="twitter-share-button" data-lang="ja" data-hashtags="rsblog">ツイート</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)) {js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
</li>
</ul>
<div style="height:20px;">
</div>
#end
</div><!-- \#main-header -->
$body
<br class="clear">
</div><!-- \#main -->
## CONTENT DIV ENDS
#if($sitemeshPage.getProperty("page.tree"))
$!sitemeshPage.getProperty("page.theme-footer")
</div>
</div>
#end
#webPanelForLocation("atl.footer" $action.context)
## $!sitemeshPage.getProperty("page.coherence-copyright")
## #foreach($property in $sitemeshPage.getPropertyKeys())
## $property = $!sitemeshPage.getProperty($property)<br />
## #end
</div><!-- \#full-height-container -->
</div><!-- \#page -->
</body>
</html>
ところで、上で書いた$変数とは何なのでしょう?実はこれはJavaオブジェクト変数です。
例えば $sitemeshPage.getProperty() という記述が多く出てきますが、sitemeshPageは com.opensymphony.module.sitemesh.Page クラスのJavaオブジェクト変数です。これらは velocity_implicit.vm ファイル(下記参照)に定義されています。(ただし、このファイルに定義されている$変数が全て使えるわけではありません)
そして$変数はJavaオブジェクトなので、メソッド呼び出しできます。例えばメインレイアウトの下から7行目付近の #foreach 文はKey=Valueで格納されている Property値を全て列挙するループ処理です。今はコメントアウトされていますが、画面描画で便利に使える値が無いか調べた名残です。
この $sitemeshPage.getPropertyKeys() や $!sitemeshPage.getProperty($property) はJavaメソッド呼び出しです。
com.opensymphony.module.sitemesh.Page クラスの JavaDocをネットで探すとメソッド仕様を見つけることができます。
velocity_implicit.vm
[root@localhost classes]# pwd /opt/atlassian/confluence/confluence/WEB-INF/classes [root@localhost classes]# more velocity_implicit.vm #* @implicitly included *# #* @vtlvariable name="actionErrors" type="java.util.Collection<java.lang.String>" *# ..... #* @vtlvariable name="helper" type="com.atlassian.confluence.themes.GlobalHelper" *# #* @vtlvariable name="i18n" type="com.atlassian.confluence.util.i18n.I18NBean" *# ... #* @vtlvariable name="req" type="javax.servlet.http.HttpServletRequest" *# #* @vtlvariable name="settingsManager" type="com.atlassian.confluence.setup.settings.SettingsManager" *# #* @vtlvariable name="sitemeshPage" type="com.opensymphony.module.sitemesh.Page" *# #* @vtlvariable name="staticResourceUrlPrefix" type="java.lang.String" *# #* @vtlvariable name="stringUtils" type="org.apache.commons.lang.StringUtils" *# ...
HTML以外にもCSSを追加することができます。そのためには「ルックアンドフィール」のスタイルシートをクリックします。以下のように書けます。ポイントは以下の点です。
スタイルシート
h1#title-heading {
line-height: normal;
}
#navigation {
display: none;
}
#space-blog-quick-search {
display: none;
}
div#blog-sidebar {
margin-top: 0;
}
h1.pagetitle {
margin: 0;
}
div.blogSurtitle {
display: inline;
padding: 0;
background-color: white;
border-width: 0;
}
#content.space {
margin-top: 0;
}
#content > div.page-metadata {
display: none;
}
div.blog-post-listing span.blogHeading a.blogHeading {
font-size: 16pt;
}
div#content div.wiki-content,
div#content div.wiki-content p,
div#content div.wiki-content li {
font-size: 10.5pt;
line-height: 14pt;
}
私は今回ボタンをつけたいと思って調査して、初めて知ったんですが、「いいね!」ボタンや「ツイート」ボタンはFacebookやTwitterが機能を提供しているのですね。それらの実装方法がJavaScriptと分かったら、あとはメインレイアウトに貼りつけるだけ。
Confluenceの良さは何と言ってもエディターだと思ってます。このエディターも上のスタイルシートで下手な設定をすると正しく動かなくなるので注意です。
今回の目的が会社ブログなので人気記事を自動表示する機能もつけたいと思いました。これはConfluenceに標準でインストールされているのですが、パフォーマンスが悪くなるという理由で初期状態OFFになっている Confluence Usage Stats というPluginを有効にしてマクロ一発で簡単に実現しました。
あとはタグクラウドもマクロで表示できます。
以外に時間がかかったのが画面右上の検索ボックスです。この会社ブログはConfluenceの1スペースで実現しています。このConfluenceでは別の情報も公開しているので、標準のクイックサーチでは他のスペースの検索結果も表示されてしまいます。ブログの検索ボックスなので、ブログ記事のみを対象に検索したいと思いました。それを実現したのがメインレイアウトの59行目 #customQuickSearch(“blog” true true [{“name”:”where”, “value”:”BLOG”}]) です。#から始まる機能はDecorator Macro として Working With Decorator Macros に説明されているのですが、動かないものもあります。
最後に
Confluenceは標準で使っても十分な機能がありますが、カスタマイズを行うことでより目的に合ったカスタマイズを加えることができます。
また有償Pluginの Zen Foundation などを使えばもっと楽にカスタマイズができるでしょう。
アトラシアン社ではサポート範囲外となっているサードパーティ製のアドオンをリックソフトのRS標準サポートではサポートします。
リックソフトのRS標準サポートは開発元が提供するサポート以上の価値があります。
ツールを導入しただけでは成功とはいえません。利用者が効果を感じていただくことが大切です。独自で制作した各種ガイドブックはツール活用を促進します。
リックソフトからライセンス購入を頂いたお客様にはガイドブックを無料進呈いたします。
ツール操作の研修だけでなく「ウォータフォール型開発」「アジャイル型開発」のシミュレーション研修も提供。
日本随一の生産性向上にも効果のある研修サービスです。
リックソフトからライセンス購入を頂いたお客様には無料招待や割引特典がございます。