技術編:ブログシステムを6年ぶりに作り直しました
このブログのシステムを、全面的に作り直しました。いま読んでいるこのページは、数日前までとは中身がまるごと別物になっています。 最初にお断りしておくと、この話は二部構成です。今回は技術の話をします。何がどう変わったのかを、実際に動くデモを交えて紹介していきます。「そもそもなぜ、noteもある時代に自分でブログを作り続けるのか」という思想の話は、次回にとっておきます。

本ブログでも過去に採用、フロントエンドフレームワーク

このブログのシステムを、全面的に作り直しました。いま読んでいるこのページは、数日前までとは中身がまるごと別物になっています。 最初にお断りしておくと、この話は二部構成です。今回は技術の話をします。何がどう変わったのかを、実際に動くデモを交えて紹介していきます。「そもそもなぜ、noteもある時代に自分でブログを作り続けるのか」という思想の話は、次回にとっておきます。
なるべくお金をかけずに、複数人でコンテンツ管理が可能な静的Webサイトが作りたくなったのでNuxt3の勉強がてら作ってみます。 システム コンテンツ 利用イメージ nuxt3-blogtest-systemの方でNuxt3製Webシステムを構築、contentディレクトリをnuxt3-blogtest-contentからgit submoduleで引っ張ってくる、著者はnuxt3-blogtest-contentだけを操作するイメージ。 手順 Nuxt3の導入 > npx nuxi init nuxt3-blogtest-system > cd nuxt3-blogtest-system > npm install @nuxt/contentの導入 > npm install --save-dev @nuxt/contentnuxt.config.tsを編集 export default defineNuxtConfig({ modules: [ '@nuxt/content' ], dir :{ public: "content/public" } }) pagesフォルダを作成 > mkdir pages > cd pages > touch index.vue > mkdir posts > cd posts > touch [...slug].vue index.vue / [...slug].vueをそれぞれ作成 index.vue <template> <div> <p> here is index.vue</p> </div> </template> [...slug].vue <template> <main> <ContentDoc /> </main> </template> content用ディレクトリを作成 // systemとは別階層で作成する > mkdir nuxt3-blogtest-content > cd nuxt3-blogtest-content > mkdir public > mkdir content > cd content > mkdir posts nuxt3-blogtest-content/content/postsディレクトリにmarkdownファイルを追加 > touch hello.md hello.md # here is hello.md 画像を利用する  //画像を使う場合はpublic/img フォルダにimg.pngという名前で画像を保存 nuxt3-blogtest-system側でnuxt3-blogtest-contentをsubmodule化 > git submodule add content コンテンツを更新した場合は、submoduleをupdate > git submodule update --remote これでnpm run dev もしくは npm run generateするとコンテンツがレンダリングされます。
この記事はmicroCMS Advent Calendar 2021 17日目の記事です。 皆さんの記事を見ていて自分も知らなかったMicroCMSの機能や使い方をしれたので、Advent Calendarはとても好きです。 個人的には@Ryusouさんの1日目の記事で、MicroCMSダッシュボードが知らない間にアップデートされていたことを知れたのが良かったです。 後はPOST入稿の際にリッチテキストをもうちょっとなんとかできると個人的に嬉しいなあ。。。
TL;DR スクロールスパイを入れたかったので、入れてみた。 もともとvue-scrolltoを入れてスムーススクロールは実装していたのだが、共存させて見たら思いのほかうまく言ってかっこよくなったので記事に残す
Qiitaとかである記事のタイトルなんかをOGPに入れ込んでシェアした段階でわかりやすくなるあれです 動的OGPを作成する手続きとしてはいくつかあるんですが、大きく分けでサーバーサイドで画像を作成するもの、image CDNを利用していい感じに作ってもらうものの2種類があります。 VOXELCANVASなんかはモデル作成後にphpで画像化してimage CDNに保存して、最後にimage CDNでロゴとblendするというハイブリッドな方法をとっています。 このブログも今まで記事ページは動的OGPを噛ませていなかったので、そういえばimgixとmicroCMSがパートナーとなったことだし、もしかしたら超簡単にできるんじゃね?ってことで挑戦してみました 手順を解説します。 まず最初に動的OGPのベースとなる画像を作成してmicroCMSにアップロードします アップロードした時点でimgixの機能が使えるようになるので、テキストなんかを自由に入力できます {{image}} あとはこれをOGPとしてNuxtで設定してあげるだけです。 head (){ return { title : this.title + " | ", meta: [ { hid: 'description', name: 'description', content: this.body.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'').substr(0, 99)+"..." }, { hid: 'og:title', property: 'og:title', content: this.title + ' | 空想は世界を包み込む' }, { hid : 'og:image', property: 'og:image', content: " { hid: 'og:url', property: 'og:url', content: ' + this.$route.path + "/" }, { hid: 'og:description', property: 'og:description', content: this.body.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'').substr(0, 99)+"..." }, ] } } 以上で動的OGPが設定できます。 imgix周りの環境構築をすべてmicroCMSがやってくれるのでとても楽でした。 今回は1行で実現したので楽でしたが複数行にする場合は少しややこしくなるようです。 →対応したので追記しました また、このブログでも何回か紹介しているimagekit.ioでは選べるフォントが少なめなのでまだimgixのほうが良さそうです。 2022/08/10追記:複数行に対応しました blendで複数行もワンラインでできることに気づき、対応しました。 {{imagemul}} og:imageを以下のように変更しています。 { hid : 'og:image', property: 'og:image', content: "
URLを貼り付けたらOGP情報があればそれを引っ張ってきて貼り付けるような仕様に対応しました
何故かmicroCMSは埋め込みが特定のサービスしか対応していないので 独自でHTMLを書いて埋め込むのができない なので埋め込みを対応できるように自力で実装しようと思う。
URLを発行したりしたときにコピーさせやすくするためのアレです こんなふうにテキストボックスを用意します <b-input ref="selectablebox" readonly id="inline-form-input-name" value="コピーしたいテキスト" @click="select($refs.selectablebox)" ></b-input> methodsにselect()を作成します select : function(target){ if(target.$el){ target.$el.select(); } } 以上。 注意点: refを利用してその後に$elで参照しているため Vueコンポーネント化されたDOMに対してのみしか動きません。 なのでデモではbootstrapのb-inputを利用しています。
ユーザージェネレーテッドかどうか YES → SSR NO ↓ ページ数が多くそれぞれのページのSEOを気にするか YES → SSG NO ↓ SPA SSR 動作が早いがデプロイ環境がnode.jsサーバーがホスティングされている必要がある。firebase hostingを利用すれば無料で環境を構築できなくもないがやや面倒。私はロリポップマネージドクラウドを利用している。 ユーザージェネレーテッドなWebアプリをJAMStack構成でやる場合、ページ数合計100ページくらいだと、Github Actionの無料枠2000分のビルドから逆算すると多くても150回程度しかCIを回せない。 そんなにページ数がない、そんなにCIを回さないのであれば可能だが、ユーザー投稿後ページとして出来上がるまでのダウンタイムはやはり無視できない SSG 基本はSSG+github actionでJAMStack構成でやっていきたい。SSGであればhosting先はgithub pagesでもいいので、ドメインさえ気にしなければ無料でできてしまう SSGを諦める理由としては、上記ユーザージェネレーテッドコンテンツの存在が大きい。 投稿者がコントロール可能なWebメディアなんかはJAMStackが最適だと思う。出力された静的ファイルをCDNでホストすれば爆速になると思う。 ただ課題もあって、1000件をこすようなコンテンツ量であればCIのコストがかかるだろうし、都度デプロイで静的サイトを足していく方法では、CIによっては過去ビルドはリフレッシュされてしまう使用のものも少なくないので気をつけたい。またJAMStackの特性として特定時間に公開といった処理は難しいのでそういった用途では選択を避けたい SPA これは正直選びどころがわからない。サーバーで動作させたくないライブラリを利用している場合(そんなときSSGでもprocess.clientで絞ればいいが。。。)とかはSPAにしとくと楽かもしれない。 やったことはないが、限られたページかつ、流入がトップからがほとんどなネイティブアプリに近いような作りで、PWAやelectronでネイティブで動作するようにし、ばらまく前提で作っているようなアプリはSPAで開発してPWAでストアにアップすることがおすすめかも知れない。
本記事はOne Tech Blogに寄稿した記事を転載したものです こんにちは GMOグローバルサイン・ホールディングス株式会社 GSP事業部 PlayCanvas推進室室長の津田@utautattaroです。 普段はPlayCanvasの事業責任者として普及促進活動をしたりWebGLのサンプルを作ったりしています。 会社では珍しく(?)フロント系のエンジニアなので、今回はフロントエンドな話題を取り上げてみました。
やりたいこと noteみたいにURLを貼ったらogpを読み取ってリッチに表示するようにする 検討している実装手段 ビルドのタイミングでbodyを直接書き換える方法を取る。 具体的に URLを検出したら以下のライブラリを利用してogp情報を取得する Nuxt.jsでのプライグインの利用方法も忘れない 課題 microCMSではaタグが使えるのだけど、aタグで貼ったときはむりやりリッチリンク化したくないので、そこは無視するようにする。 具体的に これはリッチリンク化したいURL これはしたくないURL とりあえずやってみよう
やりたいこと ↑この上の目次をデフォルトで開いてる状態にしたい 現状想定しているやり方 まずはDOMにアクセスしないといけないので、これを試す Bootstrapのイベントで取れるようなら撮ってみる だめならJavaScript書く できた bootstrap-vueのcollapseの詳細を見て解決。 toggleの項ばっかみてた
目次を作りたい 目次とは hタグをまとめて上部にアンカーリンクを置く 要件 完成物 とりあえずhタグが全部リンク化されている状態にしたい どうやって作るか? とりあえずフルスクラッチで作ろう 具体的に bodyをデータで持ってるからそれからhタグを検索。 idはmicroCMSがどうしてるのか調査したい 見出し5 見出し5って使うの?パラグラフより小さいけど まとめ とりあえず頑張って作る 作った 作り方 正規表現でタグ切り取り <h1>-<h5>までのタグを正規表現で切り取り let reg = /<h[1-5].*?<\/h[1-5]>/g; let tables = payload.post.body.match(reg); 配列に入れる microCMSだとh1-h5は自動でidを振ってくれるのでひと手間減って便利 if(tables){ for(let i=0;i<tables.length;i++){ let obj = { "tag" : tables[i].slice(1,3), "id" : '#' + tables[i].match(/".*"/)[0].slice(1).slice(0,-1), "text" : tables[i].match(/>.*</)[0].slice(1).slice(0,-1), } t.push(obj); } } 表示 横全面だとダサいのでgridでmd=4へ いい感じ <b-container class="p-0 m-0"> <b-row> <b-col md="4"> <div class="tableofcontent bg-light p-2 mb-2" v-if="t[0]"> <b-list-group> <p class="p-1 m-0 text-center"><strong>もくじ</strong></p> <b-list-group-item class="py-1" v-bind:class="'t'+table.tag" v-bind:href="table.id" v-for="table in t">{{table.text}}</b-list-group-item> </b-list-group> </div> </b-col> <b-col> </b-col> </b-row> </b-container> h1-h5でインデントをCSSでつけている .th1{ padding-left: 0.5rem !important; } .th2{ padding-left: 1.0rem !important; } .th3{ padding-left: 1.5rem !important; } .th4{ padding-left: 2.0rem !important; } .th5{ padding-left: 2.5rem !important; }結果 やったぜ
vue-highlight.jsもmarkdownitも試したがうまくいかなかったので最終手段CDNで入れたら秒でできた。 2時間返してくれ。 C++ #include <iostream> int main(int argc, char *argv[]) { /* An annoying "Hello World" example */ for (auto i = 0; i < 0xFFFF; i++) cout << "Hello, World!" << endl; char c = '\n'; unordered_map <string, vector<string> > m; m["key"] = "\\\\"; // this is an error return -2e3 + 12l; } HTML <!DOCTYPE html> <title>Title</title> <style>body {width: 500px;}</style> <script type="application/javascript"> function $init() {return true;} </script> <body> <p checked class="title" id='title'>Title</p> <!-- here goes the rest of the page --> </body> C# using System.IO.Compression; #pragma warning disable 414, 3021 namespace MyApplication { [Obsolete("...")] class Program : IInterface { public static List<int> JustDoIt(int count) { Console.WriteLine($"Hello {Name}!"); return new List<int>(new int[] { 1, 2, 3 }) } } } CSS @font-face { font-family: Chunkfive; src: url('Chunkfive.otf'); } body, .usertext { color: #F0F0F0; background: #600; font-family: Chunkfive, sans; --heading-1: 30px/32px Helvetica, sans-serif; } @import url(print.css); @media print { a[href^=http]::after { content: attr(href) } } JavaScript function $initHighlight(block, cls) { try { if (cls.search(/\bno\-highlight\b/) != -1) return process(block, true, 0x0F) + ` class="${cls}"`; } catch (e) { /* handle exception */ } for (var i = 0 / 2; i < classes.length; i++) { if (checkCondition(classes[i]) === undefined) console.log('undefined'); } return ( <div> <web-component>{block}</web-component> </div> ) } export $initHighlight; PHP require_once 'Zend/Uri/Http.php'; namespace Location\Web; interface Factory { static function _factory(); } abstract class URI extends BaseURI implements Factory { abstract function test(); public static $st1 = 1; const ME = "Yo"; var $list = NULL; private $var; /** * Returns a URI * * @return URI */ static public function _factory($stats = array(), $uri = 'http') { echo __METHOD__; $uri = explode(':', $uri, 0b10); $schemeSpecific = isset($uri[1]) ? $uri[1] : ''; $desc = 'Multi line description'; // Security check if (!ctype_alnum($scheme)) { throw new Zend_Uri_Exception('Illegal scheme'); } $this->var = 0 - self::$st; $this->list = list(Array("1"=> 2, 2=>self::ME, 3 => \Location\Web\URI::class)); return [ 'uri' => $uri, 'value' => null, ]; } } echo URI::ME . URI::$st1; __halt_compiler () ; datahere datahere datahere */ datahere CDN最高
タグに自動で色がつくようにしました。なお、同一のタグは同一の色がつくようになっています タグのコンポーネントはこんな感じ <template> <b-badge pill v-bind:style="'background-color : #'+ string_to_utf8_hex_string(tag).substr(1,6) + ';'" class="mr-2" v-bind:to="to">{{tag}}</b-badge> </template> <script> export default { props:["tag","to"], methods : { string_to_utf8_hex_string : function(text){ var bytes1 = this.string_to_utf8_bytes(text); var hex_str1 = this.bytes_to_hex_string(bytes1); return hex_str1; }, string_to_utf8_bytes : function(text){ var result = []; if (text == null) return result; for (var i = 0; i < text.length; i++) { var c = text.charCodeAt(i); if (c <= 0x7f) { result.push(c); } else if (c <= 0x07ff) { result.push(((c >> 6) & 0x1F) | 0xC0); result.push((c & 0x3F) | 0x80); } else { result.push(((c >> 12) & 0x0F) | 0xE0); result.push(((c >> 6) & 0x3F) | 0x80); result.push((c & 0x3F) | 0x80); } } return result; }, bytes_to_hex_string : function(bytes){ var result = ""; for (var i = 0; i < bytes.length; i++) { result += this.byte_to_hex(bytes[i]); } return result; }, byte_to_hex : function(byte_num){ var digits = (byte_num).toString(16); if (byte_num < 16) return '0' + digits; return digits; } } } </script> この後はワードクラウドとか作りたい