yuw27b’s blog

技術メモと雑記

Astroでトップページを最新記事にしたい

トップページは常に最新記事にしたい、つまりビルドするごとに内容が変わる(かも)という状態にしたい。 WordPressなんかでトップページを「最新の投稿」にする、みたいな設定がある思うんですが、ああいうイメージです。

Routing -> Rest parameters -> undefined でできる

結論からすると、[...slug].astroで、pathundefinedを指定するとトップレベルのindex.htmlになりました。痒いところに手が届いてて助かる……。

ドキュメント:Routing 🚀 Astro Documentation

index.astroに書けば…?

pages/index.astro に最新記事を取得して出力するコードを書いてももちろんできるんですが、個別記事を出力するファイル(つまりpages/posts/[slug].astroみたいなやつ)とほぼ同じことを書くことになるので、1つのファイルで済ませたいな、というのが前提でした。

もうちょっと具体的に

  1. pages/[...slug].astroを用意する
  2. getStaticPaths()関数に、記事を取得してきて配列を返すコードを書く
  3. 配列の最後に、最新記事をコピーしてparams.pathundefinedに変更した要素を入れておく

こんな感じ

const paths = posts.map(post => {
  //...なんか記事データの処理
  return {
    params: {path: `posts/${post.id}`},
    props: {
      //
    }
  }
})

const lastItem = Object.assign({params: {}, props: {}}, paths[paths.length - 1])
lastItem.params = {path: undefined}
paths.push(lastItem)

return paths

こうすると、pathundefinedな要素は親階層のパスになるので、/dist/index.htmlに書き出される。

JavaScriptでクリップボードにコピー(2022年版)

2017年に書いた記事が、2022年現在では非推奨になっているAPIを使っているので、改めて。

変わったところ

document.execCommand('copy')が仕様からなくなり、非推奨になりました(MDN)。今のところどのブラウザでも動作しますが、今後も動く保証はありません(とはいえ後方互換のために当面は動くと思いますが)。代わりにClipboard APIという仕様が整備されているので、新たに実装する場合はこちらを使うべきです。

HTMLとJavaScriptのコード
<p class="copyTarget">コピーしてもらいたいテキスト</p>
<button class="copyBtn">クリップボードにコピーする</button>
function copyText () {
  const $target = document.querySelector('.copyTarget');
  if (!$target) {
    return false;
  }
  const range = document.createRange();
  range.selectNode($target);
  window.getSelection().removeAllRanges();
  window.getSelection().addRange(range);

  // document.execCommand('copy'); // ←非推奨に。
  navigator.clipboard.writeText($target.innerText);

  return false;
}
document.querySelector('.copyBtn').addEventListener('click', copyText, false);
テキストエリア内のコンテンツをコピーする場合
<textarea class="textarea">テキストエリアのテキスト</textarea>
<button class="copyBtn">クリップボードにコピーする</button>
function copyText () {
  const $target = document.querySelector('.textarea');
  if (!$target) {
    return false;
  }

  $target.select();
  navigator.clipboard.writeText($target.value);

  return false;
}
document.querySelector('.copyBtn').addEventListener('click', copyText, false);
クリップボードからコピーする
<textarea class="textarea">テキストエリアのテキスト</textarea>
<button class="copyBtn">クリップボードからコピーする</button>
function copyText () {
  const $target = document.querySelector('.textarea');
  if (!$target) {
    return false;
  }

  navigator.clipboard.readText().then(clipText => {
    $target.value = clipText;
  });

  return false;
}
document.querySelector('.copyBtn').addEventListener('click', copyText, false);

ユーザーの許可を求める表示が出ますが、許可するとクリップボードにあるテキストがテキストエリアの内容に置き換わります。

ElectronでPuppeteerを使う場合の複数プラットフォーム対応

Electronで作っているデスクトップアプリ内で、Puppeteerを使った時のメモです。
結果的には、各プラットフォーム用のChromiumを手動でダウンロードして追加し、Puppeteerに対してChromiumのパスを明示的に指定する必要がありました。


npm install puppeteerして開発しているうちは問題なかったのですが、開発環境と異なるプラットフォーム用にElectronをビルドして、単体のアプリケーションとして実行するとうまくいきませんでした。

Chromium revision is not downloaded. Run "npm install" or "yarn install" at Launcher.launch

というようなエラーが出ます。Chromiumが入ってないよ、とのこと。npm installしろ、と言われても、ビルドしてしまっているのでできません。

ビルドするプラットフォーム向けのChromiumを手動で追加する

Electronをビルドし、macOS版のアプリケーションのパッケージ内容を確認すると、/Contents/Resources/app.asar.unpacked/node_modules/puppeteer以下に、.local-chromiumというディレクトリがあり、さらにその中にmac-722234というディレクトリがありました。(6ケタの数字はバージョンのようなので、環境により異なると思います。)この中にChromium本体が入っています。

私の場合はMacで開発していて、Windows用にもビルドしましたが、win-unpacked/resources/app.asar.unpacked/node_modules/puppeteer/.local-chromiumの中にはmac-722234ディレクトリしかありません。そこで、ここにwin64-722234というディレクトリを作成し、Windows用のChromiumをダウンロードしてきて入れました。ディレクトリ構造としては、win64-722234/chrome-winの中にchrome.exeなどのファイルが入っている状態です。
Chromiumのダウンロードはこちらからできます:https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html

続いて、Puppeteerの起動時にこのChromiumを見つけられるようパスを指定します。GitHubにそのままのissueがありましたので参考にしました:Run from Electron · Issue #2134 · puppeteer/puppeteer · GitHub

これでMacでもWindowsでも問題なく、単体のアプリケーション上でPuppeteerが実行できるようになりました。

また、エラーの原因を調べるにあたっては、electron-logというnpmパッケージが便利でした。こちらが詳しいです:
[Electron] ログをファイルに記録する - electron-log




最後に蛇足ながら、

  • Puppeteerは外部サイトにアクセスできるので、使い方によっては何かしらの脆弱性が生まれるかもしれません。起動時のオプションや、アクセス先の限定など、適宜設定するべきと思います。
  • Electronで1つ、Puppeteerで2つChromiumを内包したアプリケーションになるので、なんというか富豪的ではありますね…。

Web上のExcelファイルからデータを読み取る(Node.js)

備忘録。

const request = require('request');
const Excel = require('exceljs');

const getBuffer = (url) => {
  return new Promise((resolve, reject) => {
    request(url, {encoding: null}, (error, response, body) => {
      if (error !== null) {
        reject(null);
      }
      const buffer = new Buffer.from(body);
      resolve(buffer);
    });
  });
}

const buffer = await getBuffer('https://...');
if (buffer === null) {
  console.log('Couldn\'t get the file');
  return;
}

const excel = new Excel.Workbook();
await excel.xlsx.load(buffer);

const sheet = excel.worksheets[0];
const row = sheet.getRow(1);
const cellValue = row.getCell('C').value;
//...

exceljsというnpmを使った。ライセンスはMIT。
HTTP(S)リクエストのところで、{encoding: null}を入れておかないとExcelファイルとして読み込めずにエラーになる。

excel.xlsx.load()のところでawaitしてるので、asyncな関数内で使ってください。

CSS: overscroll-behavior

端までスクロールした時の振る舞いを、CSSで指定できる、というものです。

仕様:CSS Overscroll Behavior Module Level 1
対応状況:Can I use... Support tables for HTML5, CSS3, etc
MDNが分かりやすいです:
overscroll-behavior - CSS: カスケーディングスタイルシート | MDN

仕様としてはまだ草案段階で、Safariは対応していません。
ですが、「適切な場面で指定しておけばWebページが使いやすくなるが、なくても問題が起こるわけではない」タイプの機能なので、いわゆる"プログレッシブエンハンスメント"なアプローチとして採用するのは有効だと思います。

スクロールバーが入れ子になった状態で使う

MDNでは、縦に長いWebページ内に、小さなチャットウィンドウが乗っかっている例を取り上げています。ほとんどのブラウザでは、チャットを最後までスクロールしたら背後のメインコンテンツがスクロールし始めますが、overscroll-behaviorにnoneを指定すると、これを止める(スクロールを伝播させない)ことができます。

デモ:


See the Pen
vYXKWJg
by Yu Watanabe (@yuw27b)
on CodePen.

「overscroll-behavior: none」のボタンを押してから、水色のボックス内をスクロールしてみてください。端まで行っても後ろのメインコンテンツはスクロールしないはずです。

「none」は、ブラウザ既定の振る舞いも止まる

overscroll-behaviorにnoneを指定すると、上記のように外側の要素へのスクロール伝播が止まりますが、同時にスクロールが端に達した時のブラウザ既定の振る舞いが全て止まっています。例えばChromeでは上端までスクロールすると、跳ね返るようなアニメーションをしますが、body要素に{overscroll-behavior: none}を指定すると、これがなくなって、ピタッと止まります(この感覚久しぶりです)。
ブラウザ既定の振る舞いはさせたいけど、スクロール伝播は止めたい、という場合はoverscroll-behavior: containを指定すればOKです。

スマートフォンでは、「上端までスクロールしてさらに引っ張る」と、ページの再読み込みが行われますが、AndroidChromeではこの振る舞いも止まるようです(未検証)。iOSChromeは未対応のようで、いつも通り再読み込みが行われてしまいました…。

x,y 個別指定もできます

「overscroll-behavior-x」「overscroll-behavior-y」というプロパティもあります。


「背後のコンテンツまでスクロールしないほうがいいな」という状況は時々あるので、そういう時は「overscroll-behavior」が有効に使えそうです。ですが、ユーザーが期待している「いつもの動作」を止めることもできてしまうので、あまり濫用しないほうが良さそうとも思いました。