ホーム 記事一覧 ブック DH週間トピックス 検索 このサイトについて
English
Elasticsearch Search UIでの初期ソート順の指定方法

Elasticsearch Search UIでの初期ソート順の指定方法

概要 本記事はAIが作成しました。 ElasticsearchとSearch UIを使って検索インターフェースを構築する際、検索結果のソート順を制御することは一般的な要件です。このガイドでは、Search UI Reactライブラリでソートを設定する方法を説明します。 参考 https://www.elastic.co/docs/reference/search-ui/api-react-search-provider#api-react-search-provider-initial-state 初期状態とソート設定の理解 Search UIライブラリでは、検索の初期状態を指定することができ、ソートの方向とソートするフィールドを含めることができます。これは、ユーザーがページに最初にアクセスしたときに、検索結果が事前に決められた順序で表示されるようにしたい場合に特に役立ちます。 基本的なソート設定の例 Search UI設定でソートを指定する方法は次のとおりです: const config = { // その他の設定オプション... initialState: { sortDirection: 'asc', sortField: 'field_tz_id', }, }; return ( <SearchProvider config={config}> {/* 検索コンポーネント */} </SearchProvider> ); ソート設定オプション initialStateオブジェクトは、ソート関連の2つのプロパティを受け付けます: sortField : ソートしたいElasticsearchインデックス内のフィールド sortDirection : ソートの方向、'asc'(昇順)または'desc'(降順) TypeScriptの型安全性 TypeScriptを使用している場合、ソート方向を定義するときにas constを使用して型安全性を確保できます: const config = { // その他の設定... initialState: { sortDirection: 'asc' as const, // 'asc' | 'desc'として型付け sortField: 'field_tz_id', }, }; これにより、sortDirectionは「asc」または「desc」のみを取り得るようになり、潜在的なエラーを防ぐことができます。 結論 Elasticsearch Search UIで初期ソートを設定する際の参考になりましたら幸いです。

MDX.jpのオブジェクトストレージに対するIPアドレス制限の実装方法

MDX.jpのオブジェクトストレージに対するIPアドレス制限の実装方法

概要 MDX.jpのオブジェクトストレージに対するIPアドレス制限の実装方法を調べました。以下、動作確認を行なった上で、AIが記事を執筆しました。 はじめに 本記事では、MDX.jpが提供するDDN EXAScaler S3互換オブジェクトストレージサービスにおいて、特定のIPアドレスからのみアクセスを許可する設定方法について解説します。 オブジェクトストレージのセキュリティレイヤー DDN EXAScaler S3互換ストレージには、主に以下の3つのセキュリティレイヤーがあります: アクセスキーとシークレットキー :基本的な認証情報 バケットポリシー :バケットレベルでのアクセス制御 アクセス制御リスト(ACL) :オブジェクトレベルでのアクセス制御 この中で、IPアドレス制限を実装するには「バケットポリシー」を利用します。 バケットポリシーによるIPアドレス制限の設定手順 1. ポリシーJSONファイルの作成 まず、以下のようなJSONファイル(例:mdx.json)を作成します: { "Version": "2008-10-17", "Statement": [ { "Sid": "BucketName", "Effect": "Allow", "Principal": { "DDN": ["*"] }, "Action": [ "s3:ListBucket", "s3:GetObject" ], "Resource": "BucketName", "Condition": { "IpAddress": { "aws:SourceIp": [ "192.168.1.1/32", "203.0.113.0/24" ] } } } ] } ポリシーの主な要素: Version : ポリシー構文のバージョン Sid : ポリシーステートメントの識別子(任意の名前) Effect : 許可または拒否(“Allow"または"Deny”) Principal : このポリシーが適用されるユーザー(DDN EXAScalerでは"DDN"を使用) Action : 許可または拒否するアクション Resource : ポリシーが適用されるリソース(バケット名) Condition : 条件(ここでIPアドレス制限を設定) 2. ポリシーの適用 s3cmdツールを使用して、作成したポリシーをバケットに適用します: ...

Google Cloud Vision APIとGakuNin RDMを用いたTEI/XMLファイル作成アプリの試作

Google Cloud Vision APIとGakuNin RDMを用いたTEI/XMLファイル作成アプリの試作

概要 Google Cloud Vision APIとGakuNin RDMを用いたTEI/XMLファイル作成アプリを試作しましたので備忘録です。 背景 Google Cloud Vision APIを使ってOCR結果を反映したTEI/XMLファイルを作成する環境が必要になりました。そこでバックエンドとしてGakuNin RDMを用いて、ユーザごとにファイルを管理して、OCRを実行可能な環境を試作しました。 使い方 フォルダの作成 以下にアクセスします。 https://ge-manager.vercel.app/ 画面右上から、GakuNin RDMを使ってログインします。 以下のようにプロジェクト一覧が表示されます。 適当な階層まで下り、フォルダの作成ボタンを押します。 ここでは、「sample」というフォルダを作成します。 そして、「GE Manager」のリンクを押します。 以下のようなページに遷移します。 処理の実行 今回は、「e-codices - Virtual Manuscript Library of Switzerland」の「fragm1a」を使用させていただきます。 https://www.e-codices.unifr.ch/loris/gau/gau-Fragment/gau-Fragment_frag001a.jp2/full/full/0/default/jpg 画像のURLを入力して、アップロードボタンを押します。アップロードされると、以下のような画面に変わります。 次に、「OCR実行」ボタンを押します。正しく完了すると、以下のように表示されます。 次に「TEI/XML作成」ボタンを押します。正しく完了すると、以下のようにTEI/XMLとともに表示されます。 Oxygen XML Editorでダウンロードしたファイルを表示した例です。Google Cloud Vision APIによるOCR結果を確認することができます。 GakuNin RDMのファイル 上記のプロセスで作成された各種ファイルは、GakuNin RDMのフォルダにファイルとして保存されます。 参考: URLを介してアクセス可能な画像ファイルを用意する mdx.jpのオブジェクトストレージを利用して、URLを介してアクセス可能な画像ファイルを用意する。 今回はge-editorというバケットを作成し、以下のようなファイルを用意します。 { "Version": "2008-10-17", "Statement": [ { "Sid": "ge-editor", "Effect": "Allow", "Principal": { "DDN": ["*"] }, "Action": ["s3:ListBucket", "s3:GetObject"], "Resource": "ge-editor" } ] } そして、以下を実行することで、上記のバケットにアップロードされたファイルをダウンロード可能にします。 ...

「れきちず x Next.js」サイトにルートの登録機能を追加しました。

「れきちず x Next.js」サイトにルートの登録機能を追加しました。

概要 「れきちず x Next.js」サイトにルートの登録機能を追加しました。以下が表示例です。 参考 「れきちず x Next.js」サイトについては、以下で紹介しています。 この「れきちず」を使ってルートを表示する先行事例として、以下が挙げられます。 https://codh.rois.ac.jp/edomi/route/ 今回は上記の事例を参考に、ルートを作成する機能を追加しました。 使い方 サイトにアクセスします。 https://rekichizu-next.vercel.app/ja/ 「マイルートを管理」をクリックします。 ログインが求められますので、画面右上のボタンからログインします。 「新しいルートを作成」から、ルートを作成します。 以下が編集画面です。 編集アイコンをクリックすると、ルートのタイトルおよび説明を編集できます。 モードを「追加」に設定すると、地図上でクリックした箇所にピンが立ちます。ドラッグ&ドロップで移動させることも可能です。 マーカーはドラッグ&ドロップで順序変更することができます。 インポートとエクスポート エクスポート ルートの一覧画面と編集画面にダウンロードボタンを設置しています。以下のようなJSONファイルがダウンロードされます。 { "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "id": "1744639551563", "text": "湯島天神", "where": "神社", "type": "point" }, "geometry": { "type": "Point", "coordinates": [ 139.76809921470056, 35.707702013817155 ] } }, { "type": "Feature", "properties": { "id": "1744639379903", "text": "不忍池", "where": "池", "type": "point" }, "geometry": { "type": "Point", "coordinates": [ 139.77050681034103, 35.71210499496915 ] } }, { "type": "Feature", "properties": { "id": "1744639551563-1744639379903", "path": "[1744639551563] -> [1744639379903]", "type": "line" }, "geometry": { "type": "LineString", "coordinates": [ [ 139.76809921470056, 35.707702013817155 ], [ 139.77050681034103, 35.71210499496915 ] ] } } ] } これをgeojson.ioに貼り付けると、以下のように表示されます。 ...

Nuxt i18nのブラウザ言語検出を無効化する方法

Nuxt i18nのブラウザ言語検出を無効化する方法

概要 (ChatGPTによる作成) Nuxt i18nモジュールでは、ユーザーのブラウザ言語を検出して、適切な言語のページにリダイレクトする機能があります。しかし、特定の状況ではこの機能を無効化したい場合もあります。この記事では、detectBrowserLanguage: falseを使用してブラウザ言語検出を完全に無効化する方法について解説します。 https://v8.i18n.nuxtjs.org/guide/browser-language-detection 設定方法 ブラウザの言語検出機能を無効にするには、nuxt.config.jsファイルでdetectBrowserLanguageオプションをfalseに設定します。 export default defineNuxtConfig({ // その他の設定 i18n: { // 言語検出を無効化 detectBrowserLanguage: false }, // その他の設定 }) これが役立つケース 特定のURLで直接アクセスさせたい場合 : 特定のコンテンツをユーザーが直接訪れることを意図している場合、リダイレクトが邪魔になることがあります。 クロールの最適化 : 検索エンジンのクローラーが特定の言語ページに直接アクセスできるようにしたい場合。 一貫性のあるユーザーエクスペリエンス : 例えば、リンクされた言語のページ同士を行き来するユーザーに対して、予想外のリダイレクトを避けたい場合。 まとめ detectBrowserLanguage: falseを利用することで、ユーザー体験やSEO最適化のために、リダイレクトに頼らずにサイトのアクセスを制御できます。適切な状況でこの設定を利用することで、サイトのユーザビリティを向上させることができます。 ブラウザの言語設定に影響されずに、指定した言語ページをユーザーに見てもらいたいときに、この設定を試してみてください。

Next.js 15 App Router で Tailwind CSS V4 を使用してダークモードを追加する方法

Next.js 15 App Router で Tailwind CSS V4 を使用してダークモードを追加する方法

概要 Next.js 15 App Router で Tailwind CSS V4 を使用してダークモードを追加する方法です。 以下の記事が参考になりました。 https://sujalvanjare.vercel.app/blog/dark-mode-nextjs15-tailwind-v4 以下のように、ダークモードとライトモードを切り替えることができました。 上記の記事について、ChatGPTによる日本語記事です。 Next.js 15 App RouterプロジェクトでTailwind CSS V4を使用してダークモードとライトモードを実装する方法を学びましょう。このステップバイステップガイドでは、シームレスなテーマスイッチャーを実現するためのTailwind V4とNext.js 15の最新の変更点をカバーしています! 既にNext.jsプロジェクトをお持ちの場合は、ステップ1に進んでください。 ステップ1:新しいNext.jsプロジェクトを作成する App RouterとTailwind CSS v4を搭載したNext.js 15をインストールするには、次の手順に従ってください: 任意の場所にプロジェクトフォルダを作成します。 VS Codeでフォルダを開きます。 ターミナルを開き、次のコマンドを実行します: npmユーザーの場合: npx create-next-app@latest . pnpmユーザーの場合: pnpx create-next-app@latest . インストール中に、次のオプションが表示されます: What is your project named? my-app Would you like to use TypeScript? No / Yes Would you like to use ESLint? No / Yes Would you like to use Tailwind CSS? No / Yes Would you like your code inside a `src/` directory? No / Yes Would you like to use App Router? (recommended) No / Yes Would you like to use Turbopack for `next dev`? No / Yes Would you like to customize the import alias (`@/*` by default)? No / Yes What import alias would you like configured? @/* 矢印キーでオプションを選択し、Enterキーを押します。Tailwind CSSとApp Routerを必ず有効にしてください。 ...

Error: Do not use <img>. Use Image from 'next/image' instead.への対応

Error: Do not use <img>. Use Image from 'next/image' instead.への対応

概要 Error: Do not use . Use Image from ’next/image’ instead.への対応にあたり、以下の記事が参考になりました。 https://stackoverflow.com/questions/68203582/error-do-not-use-img-use-image-from-next-image-instead-next-js-using-ht 上記の記事に基づき、ChatGPTによる回答を以下に掲載します。間違っている点もあるかもしれませんが、参考になりましたら幸いです。 Next.jsのESLintルール変更とフラットコンフィグの設定方法 Next.js 11以降、ESLintの設定がデフォルトで提供されるようになり、@next/next/no-img-element ルールが追加されました。このルールにより、通常の<img>タグの使用が制限され、Next.jsのnext/imageコンポーネントを推奨するようになっています。 しかし、プロジェクトによっては<img>タグを使いたい場合もあるでしょう。そのため、このESLintルールを無効化する方法を紹介します。 1. 従来の設定方法(.eslintrc.js) 以前のNext.jsのESLint設定は、.eslintrc.js に次のように記述することで変更できました。 module.exports = { rules: { "@next/next/no-img-element": "off", }, }; しかし、Next.jsのESLintは「フラットコンフィグ(Flat Configuration)」へ移行しました。そのため、.eslintrc.js ではなく、eslint.config.mjs を使用する必要があります。 2. フラットコンフィグの設定方法(eslint.config.mjs) 現在のNext.jsプロジェクトでESLintのルールを変更するには、eslint.config.mjs ファイルを作成し、以下のように記述します。 import { dirname } from "path"; import { fileURLToPath } from "url"; import { FlatCompat } from "@eslint/eslintrc"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const compat = new FlatCompat({ baseDirectory: __dirname, }); const eslintConfig = [ ...compat.extends("next/core-web-vitals", "next/typescript"), { rules: { "@next/next/no-img-element": "off", }, }, ]; export default eslintConfig; この設定を適用することで、Next.jsのプロジェクトでも通常の<img>タグを使用できるようになります。 3. どの方法を使うべきか? Next.js 11以前のプロジェクト → .eslintrc.js を使用 Next.js 12以降のプロジェクト → eslint.config.mjs を使用 最新のNext.js環境ではフラットコンフィグを利用するのが推奨されているため、新規プロジェクトではeslint.config.mjs を設定するのがベストです。 ...

Mirador 4で、初期読み込み時に、画像の回転や範囲指定を行う

Mirador 4で、初期読み込み時に、画像の回転や範囲指定を行う

概要 Mirador 4で、初期読み込み時に、画像の回転や範囲指定を行う方法を紹介します。 背景 2025年3月現在、Mirador 4の開発が進められています。alpha版を以下で確認することができます。 https://github.com/ProjectMirador/mirador/releases おそらくMirador 4からの機能かと思いますが、以下のFAQで初期設定の方法が記述されています。 https://github.com/ProjectMirador/mirador/wiki/Frequently-Asked-Questions#q-how-do-i-change-the-view-of-an-image-to-zoom-to-a-certain-area 具体的には、以下のように、initialViewerConfigを用いることで、初期設定ができました。 https://github.com/ProjectMirador/mirador/blob/main/__tests__/integration/mirador/mirador-configs/initial-viewer-config.js export default { id: 'mirador', windows: [{ canvasId: 'https://iiif.harvardartmuseums.org/manifests/object/299843/canvas/canvas-47174892', initialViewerConfig: { thumbnailNavigationPosition: 'far-bottom', x: 934, y: 782, // you need to specify zoom for this to look good zoom: 0.0007, }, manifestId: 'https://iiif.harvardartmuseums.org/manifests/object/299843', }], }; 応用 上記を応用して、以下のような初期設定を行ってみました。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="theme-color" content="#000000"> <title>Mirador - Table of contents</title> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"> </head> <body> <div id="mirador" style="position: absolute; top: 0; bottom: 0; left: 0; right: 0;"></div> <script type="module"> import Mirador from '../../../src'; // import config from './mirador-configs/initial-viewer-config.js'; const xywh = '9554.0,8213.0,1000,1000'; const spl = xywh.split(','); // Box to zoom to const boxToZoom = { height: Number(spl[3]), width: Number(spl[2]), x: Number(spl[0]), y: Number(spl[1]) }; const zoomCenter = { x: boxToZoom.x + boxToZoom.width / 2, y: boxToZoom.y + boxToZoom.height / 2 }; const config = { id: 'mirador', windows: [{ canvasId: "https://iiif.dl.itc.u-tokyo.ac.jp/repo/iiif/187cc82d-11e6-9912-9dd4-b4cca9b10970/canvas/p2", initialViewerConfig: { rotation: 180, x: zoomCenter.x, y: zoomCenter.y, zoom: 1 / Math.max(boxToZoom.width, boxToZoom.height) }, // manifestId: 'https://purl.stanford.edu/sn904cj3429/iiif/manifest', manifestId: "https://iiif.dl.itc.u-tokyo.ac.jp/repo/iiif/187cc82d-11e6-9912-9dd4-b4cca9b10970/manifest", }], }; Mirador.viewer(config); </script> </body> </html> これにより、わかりにくいですが、以下のように、180度回転しつつ、xywhにフォーカスした形で初期読み込みできました。 ...

mdx Iのオブジェクトストレージに対するproxyサーバの構築

mdx Iのオブジェクトストレージに対するproxyサーバの構築

概要 mdx Iのオブジェクトストレージに対するproxyサーバの構築に関する備忘録です。 背景 mdx Iの利用手引きにおいて、以下のように説明されています。 https://docs.mdx.jp/ja/index.html#オブジェクトストレージ mdxが提供するS3データサービス(DDN EXAScaler S3 Data Service)で取り扱うAPI仕様を記載したDataDirect Networks社のマニュアルです。 Tipsのオブジェクトストレージの利用方法例 と合わせてご確認ください。 https://docs.mdx.jp/ja/_downloads/b5d961f2c152387fa10ed951d5278f47/S3 Data Services 5.2.7 API Reference Guide.pdf こちらをGoogle NotebookLMに読み込ませ、corsの実装状況について確認しました。以下が回答です。 回答 資料 でBucket CORS Featureが未実装 であることが明記されているため、EXAScaler Access S3 APIに対して、ウェブブラウザ上で動作するJavaScriptなどから直接、異なるオリジン (例えば、APIのエンドポイントとは異なるドメインで提供されているウェブサイトのJavaScript)を通してリクエストを行うと、CORS(Cross-Origin Resource Sharing)の問題が発生し、通常はリクエストがブロックされます 。 これは、ウェブブラウザのSame-Origin Policy というセキュリティ上の仕組みによるものです。異なるオリジンからのリソースへのアクセスは、サーバー側が明示的に許可しない限り制限されます。CORSは、この許可の仕組みを提供するものですが、EXAScaler Access S3においては、この機能がまだ実装されていないため、サーバー側からのCORSヘッダーなどのレスポンスがなく、ブラウザはクロスオリジンリクエストを拒否する と考えられます。 したがって、プロキシサーバー などを利用して、クライアントサイドのリクエストを一旦プロキシサーバーに送り、そこからEXAScaler Access S3 APIにリクエストを行うことで、オリジンが一致するように見せかけるなどの回避策が必要になる場合があります。プロキシサーバーは、APIからのレスポンスに必要なヘッダーを付与する役割を担うこともできます。 ただし、これはCORSの問題を回避するための一つの一般的な方法であり、他にもサーバーサイドでAPIを公開する際にオリジンを一致させるなどの方法も考えられます。重要なのは、EXAScaler Access S3 API自体がCORSの仕組みをサポートしていない ため、クライアントサイドから直接クロスオリジンリクエストを行う際には、何らかの対策が必要になるということです。 対策 理解が間違っている点もあるかもしれませんが、上記の回答から、mdx Iのオブジェクトストレージに対するproxyサーバの構築を行うこととしました。 構築したproxyサーバのリポジトリは以下です。 https://github.com/nakamura196/s3-proxy 例えば、以下で指定したバケット上のファイルにアクセスできます。 https://s3-proxy.vercel.app/public/CETEIcean.css 一方、以下が直接アクセスした例です。 https://s3ds.mdx.jp/satoru196/public/CETEIcean.css 前者では、以下のようなレスポンスヘッダーが確認でき、CORSの設定がなされていることが確認できます。 access-control-allow-origin: * age: 0 cache-control: public, max-age=0, must-revalidate content-encoding: br content-type: text/css date: Sun, 23 Mar 2025 03:11:27 GMT etag: W/“3d5c-psb8jsgRM3DKYLgMVwi7Ns/AIhg” server: Vercel strict-transport-security: max-age=63072000; includeSubDomains; preload x-powered-by: Express x-vercel-cache: MISS x-vercel-id: hnd1::iad1::zbdgm-1742699486592-2b286aacd062 実装 Expressを用いて、以下のように実装しました。aws-sdkについては、AWS SDK for JavaScript v3に移行する必要があるようなので、この点はご注意ください。 ...

Next.jsでUniversal Viewerのnpmパッケージを使用する

Next.jsでUniversal Viewerのnpmパッケージを使用する

概要 Next.jsでUniversal Viewerのnpmパッケージを使用する方法の備忘録です。 インストール 以下でインストールします。 npm i universalviewer 実装 useEffectを使うため、クライアントコンポーネントとして実装する必要があるようでした。 またdivタグにuvクラスを与えることで、cssが当たるようになりました。 'use client' import { useEffect } from 'react' import dynamic from 'next/dynamic' interface ViewerProps { manifest: string cv?: number xywh?: string } // コンポーネントの実装 function ViewerComponent({ manifest, cv, xywh }: ViewerProps) { useEffect(() => { // universalviewerをインポートして初期化 const { init } = require('universalviewer') require('universalviewer/dist/esm/index.css') init('uv', { manifest, canvasIndex: cv, xywh }) }, [manifest, cv, xywh]) return ( <div id="uv" className="uv" style={{ width: '100%', height: '60vh' }}></div> ) } // SSRを無効化し、クライアントサイドでのみレンダリングするコンポーネント const Viewer = dynamic(() => Promise.resolve(ViewerComponent), { ssr: false, loading: () => ( <div style={{ width: '100%', height: '60vh', display: 'flex', justifyContent: 'center', alignItems: 'center', background: '#f0f0f0', }} > ビューワーを読み込み中... </div> ), }) export default Viewer 他にも使用可能なオプションがあるかと思いますが、cvで処理ロードするcanvasのインデックス、xywhで表示する矩形を指定することができました。 まとめ Universal Viewerの利用にあたり、参考になりましたら幸いです。

GakuNin RDMのAPIを用いて、連携したストレージのファイルを検索する

GakuNin RDMのAPIを用いて、連携したストレージのファイルを検索する

概要 以下の記事で、GakuNin RDMのAPIを用いたアプリケーション構築について紹介しました。 本記事では、GakuNin RDMのAPIを用いて、連携したストレージのファイルを検索する方法を紹介します。 実装例 次のような形で、検索APIを実装しました。なお、https://rdm.nii.ac.jp/api/v1/search/file/にクライアントから直接アクセスした際には、CORSによるエラーが発生したため、Next.jsのAPI Routesとして実装しています。 import { NextResponse } from "next/server"; import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { getServerSession } from "next-auth"; export async function GET(req: Request) { const session = await getServerSession(authOptions); // URLからクエリパラメータを取得 const url = new URL(req.url); const query = url.searchParams.get("filter[fulltext]") || ""; const offset = parseInt(url.searchParams.get("page[offset]") || "0", 10); const size = parseInt(url.searchParams.get("page[limit]") || "20", 10); const accessToken = session?.accessToken; const apiUrl = "https://rdm.nii.ac.jp/api/v1/search/file/"; const params = { api_version: { vendor: "grdm", version: 2 }, sort: "created_desc", highlight: "title:30,name:30,user:30,text:124,comments.*:124", elasticsearch_dsl: { query: { filtered: { query: { query_string: { default_field: "_all", fields: [ "_all", "title^4", "description^1.2", "job^1", "school^1", "all_jobs^0.125", "all_schools^0.125", ], query, analyze_wildcard: true, lenient: true, }, }, }, }, from: offset, size, }, }; const res = await fetch(apiUrl, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify(params), }); const data = await res.json(); return NextResponse.json(data); } 利用例 以下のURLからお試しいただけます。(利用にあたっては、GakuNin RDMへのログインが必要です。) ...

LEAF WriterとGakuNin RDMを用いたTEI/XMLファイルの編集環境の試作

LEAF WriterとGakuNin RDMを用いたTEI/XMLファイルの編集環境の試作

概要 LEAF WriterとGakuNin RDMを用いたTEI/XMLファイルの編集環境の試作を行いましたので、備忘録です。 参考 以下の記事で、LEAF WriterをNext.jsから使用する方法を紹介しました。 特に、以下のnpmパッケージを使用しています。 https://www.npmjs.com/package/@cwrc/leafwriter 上記で編集対象とするTEI/XMLファイルの入出力にあたり、GakuNin RDMを使用してみます。GakuNin RDMのAPIをJavaScriptから使用する方法について、以下も参考になりましたら幸いです。 使い方 以下がプロトタイプシステムのURLです。(色々と不具合が含まれる点にご注意ください。) https://rdm-leaf-editor.vercel.app/ UIはClaude 3.7 Sonnetに作成してもらっています。 「サインイン」ボタンを押すと、認証画面に進むので、ログインします。 ログイン後、リダイレクトされ、プロジェクトの一覧が表示されます。 TEI/XMLファイルが含まれるディレクトリまで移動します。ファイル名に「.xml」が含まれる場合、「Leaf Writer」の列に「編集」ボタンが表示されます。 LEAF Writerの編集画面に遷移するので、テキストを編集します。作業が完了したら、画面右上の「保存」ボタンを押します。 GakuNin RDMのUIから確認してみると、バージョンごとに保存されていることが確認できます。 実装 GakuNin RDMからのファイルの取得および更新は以下で行っています。 /** * ファイルの内容を取得する */ export async function fetchFileContent( url: string, accessToken: string ): Promise<string> { const response = await fetch(url, { method: "GET", headers: { Authorization: `Bearer ${accessToken}`, }, }); if (!response.ok) { throw new Error( `ファイルの取得に失敗しました。ステータスコード: ${response.status}` ); } return await response.text(); } /** * ファイルの内容を更新する */ export async function updateFileContent( url: string, content: string, accessToken: string, contentType: string = "application/xml" ): Promise<void> { const blob = new Blob([content], { type: contentType }); const response = await fetch(url, { method: "PUT", headers: { Authorization: `Bearer ${accessToken}`, }, body: blob, }); if (!response.ok) { const errorText = await response.text(); console.error("保存に失敗しました。ステータスコード:", response.status); console.error("レスポンス:", errorText); throw new Error(`保存に失敗しました。ステータスコード: ${response.status}`); } } 上記で使用するURLは以下です。 ...

GakuNin RDM(OSF)のAPIで、フィルタを使う

GakuNin RDM(OSF)のAPIで、フィルタを使う

概要 GakuNin RDM(OSF)のAPIで、フィルタを使う方法の備忘録です。 対象データ 以下のようなファイル構造を持つ「NII Storage」を対象にします。 APIでは、以下のようなURLでアクセスできるものを対象にします。 https://api.rdm.nii.ac.jp/v2/nodes/wzv9g/files/osfstorage/ JSONデータの例は以下です。 { "data": [ { "id": "67ce5b0b2fe4740010f753c0", "type": "files", "attributes": { "guid": "ungd3", "checkout": null, "name": "IMG_8269.png", "kind": "file", "path": "/67ce5b0b2fe4740010f753c0", "size": 952107, "provider": "osfstorage", "materialized_path": "/IMG_8269.png", "last_touched": null, "date_modified": "2025-03-10T03:22:51.750550Z", "date_created": "2025-03-10T03:22:51.750550Z", "extra": { "hashes": { "md5": "e57192b30103a7e995597ceaea39cbbf", "sha256": "5e282187067a53aaab0f1f00daaefb9519d60b064831403e671662cfbcf6f41f" }, "downloads": 0 }, "tags": [], "current_user_can_comment": true, "current_version": 1 }, "relationships": { "parent_folder": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/files/674034a483bdc200108b8a95/?format=json", "meta": {} } }, "data": { "id": "674034a483bdc200108b8a95", "type": "files" } }, "versions": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/files/67ce5b0b2fe4740010f753c0/versions/?format=json", "meta": {} } } }, "comments": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/nodes/wzv9g/comments/?format=json&filter%5Btarget%5D=ungd3", "meta": {} } } }, "metadata_records": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/files/67ce5b0b2fe4740010f753c0/metadata_records/?format=json", "meta": {} } } }, "node": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/nodes/wzv9g/?format=json", "meta": {} } }, "data": { "id": "wzv9g", "type": "nodes" } }, "target": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/nodes/wzv9g/", "meta": { "type": "node" } } }, "data": { "type": "node", "id": "wzv9g" } } }, "links": { "info": "https://api.rdm.nii.ac.jp/v2/files/67ce5b0b2fe4740010f753c0/", "move": "https://files.rdm.nii.ac.jp/v1/resources/wzv9g/providers/osfstorage/67ce5b0b2fe4740010f753c0", "upload": "https://files.rdm.nii.ac.jp/v1/resources/wzv9g/providers/osfstorage/67ce5b0b2fe4740010f753c0", "delete": "https://files.rdm.nii.ac.jp/v1/resources/wzv9g/providers/osfstorage/67ce5b0b2fe4740010f753c0", "download": "https://rdm.nii.ac.jp/download/ungd3/", "render": "https://mfr.rdm.nii.ac.jp/render?url=https://rdm.nii.ac.jp/download/ungd3/?direct%26mode=render", "html": "https://rdm.nii.ac.jp/wzv9g/files/osfstorage/67ce5b0b2fe4740010f753c0", "self": "https://api.rdm.nii.ac.jp/v2/files/67ce5b0b2fe4740010f753c0/" } }, { "id": "67da847416000900109e0454", "type": "files", "attributes": { "guid": "b45mp", "checkout": null, "name": "01.xml", "kind": "file", "path": "/67da847416000900109e0454", "size": 79397, "provider": "osfstorage", "materialized_path": "/01.xml", "last_touched": null, "date_modified": "2025-03-19T13:24:27.868078Z", "date_created": "2025-03-19T08:46:44.636107Z", "extra": { "hashes": { "md5": "a3824b2f49471842d1046a2abe623284", "sha256": "83d18a6e52a52597ebac6fa1eb8a137ed6e1e64b9c0e2c1a0b49cf746a777d0a" }, "downloads": 0 }, "tags": [], "current_user_can_comment": true, "current_version": 5 }, "relationships": { "parent_folder": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/files/674034a483bdc200108b8a95/?format=json", "meta": {} } }, "data": { "id": "674034a483bdc200108b8a95", "type": "files" } }, "versions": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/files/67da847416000900109e0454/versions/?format=json", "meta": {} } } }, "comments": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/nodes/wzv9g/comments/?format=json&filter%5Btarget%5D=b45mp", "meta": {} } } }, "metadata_records": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/files/67da847416000900109e0454/metadata_records/?format=json", "meta": {} } } }, "node": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/nodes/wzv9g/?format=json", "meta": {} } }, "data": { "id": "wzv9g", "type": "nodes" } }, "target": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/nodes/wzv9g/", "meta": { "type": "node" } } }, "data": { "type": "node", "id": "wzv9g" } } }, "links": { "info": "https://api.rdm.nii.ac.jp/v2/files/67da847416000900109e0454/", "move": "https://files.rdm.nii.ac.jp/v1/resources/wzv9g/providers/osfstorage/67da847416000900109e0454", "upload": "https://files.rdm.nii.ac.jp/v1/resources/wzv9g/providers/osfstorage/67da847416000900109e0454", "delete": "https://files.rdm.nii.ac.jp/v1/resources/wzv9g/providers/osfstorage/67da847416000900109e0454", "download": "https://rdm.nii.ac.jp/download/b45mp/", "render": "https://mfr.rdm.nii.ac.jp/render?url=https://rdm.nii.ac.jp/download/b45mp/?direct%26mode=render", "html": "https://rdm.nii.ac.jp/wzv9g/files/osfstorage/67da847416000900109e0454", "self": "https://api.rdm.nii.ac.jp/v2/files/67da847416000900109e0454/" } }, { "id": "67daca9916000900109e1d98", "type": "files", "attributes": { "guid": null, "checkout": null, "name": "test", "kind": "folder", "path": "/67daca9916000900109e1d98/", "size": null, "provider": "osfstorage", "materialized_path": "/test/", "last_touched": null, "date_modified": null, "date_created": null, "extra": { "hashes": { "md5": null, "sha256": null } }, "tags": [], "current_user_can_comment": true, "current_version": 1 }, "relationships": { "parent_folder": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/files/674034a483bdc200108b8a95/?format=json", "meta": {} } }, "data": { "id": "674034a483bdc200108b8a95", "type": "files" } }, "files": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/nodes/wzv9g/files/osfstorage/67daca9916000900109e1d98/?format=json", "meta": {} } } }, "node": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/nodes/wzv9g/?format=json", "meta": {} } }, "data": { "id": "wzv9g", "type": "nodes" } }, "target": { "links": { "related": { "href": "https://api.rdm.nii.ac.jp/v2/nodes/wzv9g/", "meta": { "type": "node" } } }, "data": { "type": "node", "id": "wzv9g" } } }, "links": { "info": "https://api.rdm.nii.ac.jp/v2/files/67daca9916000900109e1d98/", "move": "https://files.rdm.nii.ac.jp/v1/resources/wzv9g/providers/osfstorage/67daca9916000900109e1d98/", "upload": "https://files.rdm.nii.ac.jp/v1/resources/wzv9g/providers/osfstorage/67daca9916000900109e1d98/", "delete": "https://files.rdm.nii.ac.jp/v1/resources/wzv9g/providers/osfstorage/67daca9916000900109e1d98/", "new_folder": "https://files.rdm.nii.ac.jp/v1/resources/wzv9g/providers/osfstorage/67daca9916000900109e1d98/?kind=folder", "self": "https://api.rdm.nii.ac.jp/v2/files/67daca9916000900109e1d98/" } } ], "links": { "first": null, "last": null, "prev": null, "next": null, "meta": { "total": 3, "per_page": 10 } } } 検索例 JSON:APIに準拠しているので、filterパラメータを使用します。 ...

Nuxt Content: Cannot find name 'queryContent'.への対応

Nuxt Content: Cannot find name 'queryContent'.への対応

概要 Nuxt Contentにおいて、「Cannot find name ‘queryContent’.」というエラーが発生しましたので、対処方法に関する備忘録です。 原因 2025/1/16にNuxt Content v3がリリースされたようです。 https://content.nuxt.com/blog/v3 これにより、queryContentはqueryCollectionなどに変更されたようです。 対処方法 以下に記載があるように、content.config.tsを作成した上で、queryCollectionなどを使用するように変更する必要があるようです。 https://content.nuxt.com/blog/v3#️-content-collections 上記の対応により、エラーを解消できました。 まとめ Nuxt Content v2からv3への移行にあたり、参考になりましたら幸いです。

AWSのRoute 53で設定したレコードを、さくらレンタルサーバで使用する(共有SSL)

AWSのRoute 53で設定したレコードを、さくらレンタルサーバで使用する(共有SSL)

概要 AWSのRoute 53で設定したレコードを、さくらレンタルサーバで使用する備忘録です。加えて、Let’s Encryptを用いて、無料SSLを使用します。 さくらレンタルサーバ ドメイン/SSLにアクセスして、「ドメイン新規追加」ボタンを押します。 画面下部の「他社で取得したドメインを移管せずに使う」の「追加」ボタンを押します。 独自ドメインを入力して、「追加」ボタンを押します。以下の例では、「aaa.example.org」としています。 追加後、追加したドメイン名の「設定」>「DNSレコード設定」を押し、AレコードのIPアドレスを控えます。 AWSのRoute 53 先ほど控えたIPアドレスを使って、レコードを追加します。 さくらレンタルサーバ SSLの設定を行います。 「SSL証明書の種類を選択」ボタンを押します。 「Let’s Encrypt (無料SSL)」の「利用する」ボタンを押します。 設定後、以下のような画面になります。少し待ちます。 その後、ドメイン設定において、以下のように設定します。 さらに、それぞれのドメインに対して、「WEB公開フォルダ」を設定することで、ルートディレクトリを変更することができました。 まとめ AWSのRoute 53以外でも同様の手続きを行うことができると思います。 間違っている点もあるかもしれませんが、さくらレンタルサーバでの独自ドメインおよびSSLの利用にあたり、参考になりましたら幸いです。

Miradorで画像を表示し、CETEIceanでテキストを表示するサンプルアプリ

Miradorで画像を表示し、CETEIceanでテキストを表示するサンプルアプリ

概要 TEI/XMLファイルを読み込み、Miradorで画像を表示し、CETEIceanでテキストを表示するサンプルアプリを作成しました。以下のURLからお試しいただけます。 デモサイト https://nakamura196.github.io/ceteicean-mirador/ 背景 これまでにも、同様の機能を提供するアプリケーションを開発してきました。 Next.js を使用した実装例 XSLT を使用した実装例 今回は、HTMLとプレーンなJavaScriptのみを使用して実装する方法をご紹介します。 対象データ 以下の校異源氏物語テキストDBを対象とします。 https://kouigenjimonogatari.github.io/ 実装方法 ソースコードは以下のリポジトリで公開しています。 https://github.com/nakamura196/ceteicean-mirador 実装のポイント 1. CETEIcean の behaviors を利用した pb タグの処理 以下のコードでは、CETEIcean の behaviors を利用して pb タグのクリック時の挙動を定義しています。 ct.addBehaviors({ tei: { // 不要な要素を非表示 graphic: () => document.createDocumentFragment(), figure: () => document.createDocumentFragment(), // pbタグの処理 pb: function (el) { let pb = document.createElement("tei-pb"); // 属性を引き継ぐ if (el.hasAttribute("n")) { pb.setAttribute("n", el.getAttribute("n")); } // corresp属性からzoneIdを取得 const corresp = el.getAttribute("corresp"); if (corresp) { const zoneId = corresp.replace('#', ''); pb.setAttribute("data-zone-id", zoneId); } // ページ番号を表示 const pageNum = el.getAttribute("n") || ""; pb.textContent = `[Page ${pageNum}]`; // クリックイベントを追加 pb.addEventListener("click", function() { const zoneId = this.getAttribute("data-zone-id"); if (zoneId) { const zoneElement = document.querySelector(`tei-zone[id="${zoneId}"]`); if (zoneElement) { const surfaceElement = zoneElement.closest("tei-surface"); if (surfaceElement && surfaceElement.hasAttribute("sameAs")) { goToPage(surfaceElement.getAttribute("sameAs")); } } } }); return pb; } }, }); 2. Mirador でのページ遷移処理 pb タグをクリックした際に、TEI/XML ファイルから Canvas の URI を取得し、Mirador のページ遷移を実行します。 ...

AtoM(Access to Memory)のAPIを使って、オブジェクトを登録してみる

AtoM(Access to Memory)のAPIを使って、オブジェクトを登録してみる

概要 AtoM(Access to Memory)のAPIを使って、オブジェクトを登録する方法の備忘録です。 APIの有効化 以下にアクセスします。 /sfPluginAdminPlugin/plugins arRestApiPluginを有効にします。 APIキーの取得 以下に、APIキーを生成する方法が説明されています。 https://www.accesstomemory.org/en/docs/2.9/dev-manual/api/api-intro/#generating-an-api-key-for-a-user ユーザ名とパスワードでもAPI接続できるようですが、今回はREST API Keyを発行しました。 エンドポイント AtoMでは、「典拠レコード」や「機能」など、複数のメニューが提供されていますが、APIによって利用できるのは、以下のみのようです。 See the subsequent pages for more details on each endpoint, and available parameters. There are three endpoints available: Browse taxonomy terms Browse information objects Read information object Download digital objects Add physical objects この点は、ArchivesSpaceのほうが豊富なAPIが提供されており、軍配が上がるかもしれません。 https://archivesspace.github.io/archivesspace/api/ また、以下のソースコードを確認すると、CreateActionが可能なものは、informationobjectsとphysicalobjects、digitalobjectsに限定されているようでした。 https://github.com/artefactual/atom/tree/qa/2.x/plugins/arRestApiPlugin/modules/api/actions ただ機械的に一括登録を行いたい場面は、主にinformationobjectsだと考えられるため、これらの機能のみで十分かもしれません。 physical objectsの登録 以下のようなクラスを用意します。 #| export class ApiClient: def __init__(self): load_dotenv(override=True) self.url = os.getenv("atom_url") username = os.getenv("username") password = os.getenv("password") api_key = os.getenv("api_key") if api_key: self.headers = { "REST-API-Key": api_key, "Content-Type": "application/json" } else: # Basic 認証のヘッダーを作成 auth_string = f"{username}:{password}" auth_bytes = auth_string.encode('ascii') auth_b64 = base64.b64encode(auth_bytes).decode('ascii') self.headers = { "Authorization": f"Basic {auth_b64}", "Content-Type": "application/json" } def add_physical_objects(self, physical_objects): url = f"{self.url}/api/physicalobjects" print(url, self.headers, physical_objects) response = requests.post(url, headers=self.headers, json=physical_objects) # レスポンスを確認 if response.status_code in [200, 201]: print("物理オブジェクトが作成されました!") print(f"ステータスコード: {response.status_code}") print(f"レスポンス: {response.text}") # 作成されたオブジェクトの情報 result = response.json() print(f"作成された物理オブジェクトID: {result.get('id')}") print(json.dumps(result, indent=4)) else: print(f"エラー: {response.status_code}") print(f"レスポンス: {response.text}") 以下で実行します。 ...

AtoM(Access to Memory)をDockerで起動する

AtoM(Access to Memory)をDockerで起動する

概要 AtoM(Access to Memory)をDockerで起動する機会があったので、備忘録です。 マニュアル 以下に記載があります。 https://www.accesstomemory.org/es/docs/2.9/dev-manual/env/compose/ git clone -b qa/2.x https://github.com/artefactual/atom.git atom cd atom export COMPOSE_FILE="$PWD/docker/docker compose.dev.yml" docker compose up -d そして、以下を実行します。 docker compose exec atom php symfony tools:purge --demo これにより、63001ポートでAtoMが起動しました。 その他、マニュアルにはCompile Theme Filesの記述などがありますが、これを実行しなくても起動する場合と起動しない場合がありました。 日本語化 以下にアクセスして、日本語を追加します。 /settings/language そして、以下を実行します。 docker compose exec atom php symfony search:populate 結果、以下のように日本語が追加されました。 まとめ AtoMの利用にあたり、参考になりましたら幸いです。

ピラミッドTIFFの作成において、ImageMagickがうまく動作しないケースがある?

ピラミッドTIFFの作成において、ImageMagickがうまく動作しないケースがある?

概要 IIIFの画像配信に向けたピラミッドTIFFの作成において、ImageMagickがうまく動作しないケースがあり、調査してみました。 参考 以下のようなページで、変換方法が説明されています。 https://samvera.github.io/serverless-iiif/docs/source-images#creating-tiled-tiffs Using the VIPS command line # For a 3-channel source image vips tiffsave source_image.tif output_image.tif --tile --pyramid --compression jpeg --tile-width 256 --tile-height 256 # For a source image with an alpha channel vips extract_band source_image.tif temp_image.v 0 --n 3 \ && vips tiffsave temp_image.v output_image.tif --tile --pyramid --compression jpeg --tile-width 256 --tile-height 256 \ && rm temp_image.v Using ImageMagick convert source_image.tif -alpha off \ -define tiff:tile-geometry=256x256 \ -define tiff:generate-pyramids=true \ -compress jpeg \ 'ptif:output_image.tif' 対象データ 以下の画像を使用させていただきます。 ...

mdx.jpのオブジェクトストレージとIIP Image(IIIF Image Server)を使ってIIIF画像を配信する

mdx.jpのオブジェクトストレージとIIP Image(IIIF Image Server)を使ってIIIF画像を配信する

概要 mdx.jpのオブジェクトストレージとIIP Image(IIIF Image Server)を使ってIIIF画像を配信する試行の備忘録です。 以下の記事の続きです。 Docker版IIP Image 以下で、IIPImage serverのDocker Imageが公開されていましたので、こちらを使います。 https://hub.docker.com/r/iipsrv/iipsrv 以下の記事などを参考に、Dockerをインストールします。 https://qiita.com/Marron-chan/items/570c7c7baaae3b4d6b11 実行 前回の記事に倣い、以下のようにmdx.jpのオブジェクトストレージをマウントします。 s3fs satoru196 ~/s3mount -o passwd_file=~/.passwd-s3fs -o url=https://s3ds.mdx.jp -o use_path_request_style -o allow_other 注意点として、前回の記事から、-o allow_otherを追加しています。これを追加しないと、次のコンテナ起動時に以下のエラーが発生しました。 docker: Error response from daemon: error while creating mount source path '~/s3mount/iip/images': mkdir ~/s3mount: file exists Run 'docker run --help' for more information -o allow_otherオプションを追加した上で、以下を実行します。無事に起動しました。 docker run -it -p 9000:9000 -p 8080:80 -v ~/s3mount/iip/images/:/images --rm iipsrv/iipsrv <-----------------------------------> Thu Mar 6 22:35:43 2025 IIPImage Server. Version 1.2 *** Ruven Pillay <ruven@users.sourceforge.net> *** Verbosity level set to 5 Running in standalone mode on socket: 0.0.0.0:9000 with backlog: 2048 Setting maximum image cache size to 10MB Setting filesystem prefix to '/images/' Setting filesystem suffix to '' Setting default JPEG quality to 75 Setting default PNG compression level to 1 Setting default WebP compression level to 50 Setting maximum CVT size to 5000 Setting HTTP Cache-Control header to 'max-age=86400' Setting 3D file sequence name pattern to '_pyr_' Setting default IIIF Image API version to 3 Setting Allow Upscaling to true Setting ICC profile embedding to true Setting up JPEG2000 support via OpenJPEG Setting image processing engine to CPU processor OpenMP enabled for parallelized image processing with 2 threads Setting URI mapping to iiif=>IIIF. Supported protocol: IIIF Memcached support enabled. Connected to servers: 'localhost' with timeout 86400 Initialisation Complete. <-----------------------------------> そして、今回の設定では、オブジェクトストレージの/satoru196/iip/imagesにtiled multi-resolution pyramid TIFFファイルを格納し、以下のようなURLでアクセスできることを確認します。 ...