このブログの技術スタックについて

はじめに

このブログはFlutter×Firebase Hostingで実装しています。
今回はFlutterではどんなライブラリを使っているのか、またブログ部分はどう実現しているのかを書ければと思います。

使用しているライブラリ

主に下記のライブラリを使っていおり、至って普通です。
全て有名なライブラリなので説明は不要ですねw

ブログ部分の実装

まずAPIを用意するのも面倒ということもあり、データは全て内部で持っています。

実現方法ですが、
Markdown用のディレクトリ作成し、その中にMarkdown形式のブログを入れています。
そして自作のスクリプトを叩くことによってディレクトリに入っているMarkdownファイル達をJSON化します。
そのJSONをFlutter側で使っています。

Markdownの形式や自作スクリプトに関して、詳しく説明します。

Markdownファイルですが、上にメタデータである title: <title> , description: <description> , slug: <slug> , created_at: <2023-12-10> , tags: <hoge,fuga> を記載し 、上下を --- で囲います。
その下に本文を記載します。
(Markdownのファイルを貼りたかったのですが、スクリプトがおかしくなるため割愛させてくださいmm)

スクリプトは下記です。
今回はFlutterで作っていることもあり、Dartでスクリプトを作っています。

import 'dart:convert';
import 'dart:io';

void main(List<String> args) {
  if (args.length != 2) {
    print('使い方: dart convert_folder_to_json.dart inputPath outputFile');
    return;
  }

  // 対象とするフォルダ
  final folderPath = args[0];
  // アウトプット先
  final outputFile = args[1];
  // MarkdownのファイルをJSON化した結果を返す
  final result = _convertFilesToJson(folderPath);

  // JSONの書き込み
  _writeToJsonFile(outputFile, {'blogs': result});
  print('JSON ファイルが作成されました: $outputFile');
}

List<Map<String, dynamic>> _convertFilesToJson(String folderPath) {
  final result = <Map<String, dynamic>>[];
  try {
    // フォルダ内のファイル一覧を取得
    final files = Directory(folderPath).listSync();
    // フォルダ内の各ファイルに対して処理
    for (final file in files) {
      if (file is File && file.path.endsWith('.md')) {
        // 1つのMarkdownファイルをJSON化
        final fileData = _convertFileToJson(file.path);
        result.add(fileData);
      }
    }
  } on Exception catch (e) {
    print('エラーが発生しました: $e');
  }
  return result;
}

Map<String, dynamic> _convertFileToJson(String filePath) {
  // ファイルの読み込み
  final lines = File(filePath).readAsLinesSync();
  final startSeparatorIndex = lines.indexOf('---');
  final endSeparatorIndex = lines.lastIndexOf('---');

  if (startSeparatorIndex != -1 && endSeparatorIndex != -1 && startSeparatorIndex < endSeparatorIndex) {
    // メタデータ部分を `_MetaData` に変換
    final metadataLines = lines.sublist(startSeparatorIndex + 1, endSeparatorIndex);
    final metadata = _MetaData.from(metadataLines.join('\n'));

    // 本文を1行にする
    final contentLines = lines.sublist(endSeparatorIndex + 1);
    final content = contentLines.join('\n').trimLeft();

    return {
      'metadata': metadata.toMap(),
      // `resource/~` は内部の画像をブログ内で表示させるために変換しています
      'content': content.replaceAll('![](assets/', '![](resource:assets/'),
    };
  } else {
    print('区切り線が正しくありません。ファイル: $filePath');
    return {};
  }
}

void _writeToJsonFile(String outputFile, Map<String, dynamic> jsonData) {
  File(outputFile).writeAsStringSync(jsonEncode(jsonData));
}

class _MetaData {
  const _MetaData({
    required this.title,
    required this.description,
    required this.slug,
    required this.createdAt,
    required this.tags,
  });

  factory _MetaData.from(String metaDataString) {
    final lines = LineSplitter.split(metaDataString).where((line) => line.isNotEmpty);
    final map = <String, dynamic>{};

    for (final line in lines) {
      final parts = line.split(':');
      if (parts.length >= 2) {
        final key = parts[0].trim();
        final value = parts.sublist(1).join(':').trim();
        map[key] = value;
      }
    }

    return _MetaData(
      title: map['title'] as String,
      description: map['title'] != null ? map['title'] as String : null,
      slug: map['slug'] as String,
      createdAt: DateTime.parse('${map['created_at'] as String}T00:00:00+09:00'),
      tags: (map['tags'] as String).split(','),
    );
  }

  final String title;
  final String? description;
  final String slug;
  final DateTime createdAt;
  final List<String> tags;

  Map<String, dynamic> toMap() {
    return {
      'title': title,
      'description': description,
      'slug': slug,
      'created_at': createdAt.toIso8601String(),
      'tags': tags,
    };
  }
}

後はこれを叩いてMarkdownのファイルをJSON化させます。

dart convert_markdowns_to_json.dart ${CURRENT_DIR}/markdown ${CURRENT_DIR}/assets/blog/blogs.json

そうするとこのようなJSONが生成されます。

{
  "blogs": [
    {
      "metadata": {
        "title": "title",
        "description": "description",
        "slug": "test",
        "created_at": "2023-12-11T15:00:00.000Z",
        "tags": [
          "hoge",
          "fuga"
        ]
      },
      "content": "## h2\n\ntext"
    }
  ]
}

後はこちらをFlutter側で読み取ってflutter_markdownを使って表示しています。
またタグも tags を使って表示しています。

まとめ

いかがでしたでしょうか。
APIを用意したくない&効率的に運用できる方法を考えたところ、Markdownファイルを自作スクリプトでJSONに変える方向にしました。
もしブログなどMarkdownを使用したい方がいましたら参考にしていただければと思います。