rarala2020
@rarala2020 (ら らら)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

エムグラムのようなUGCコンテンツの結果ページURLの仕組みが知りたい

Q&A

Closed

エムグラム #私を構成する8性格 のようにユーザーが入力した内容に応じて結果ページを表示し、
そのページをSNSボタンからシェアさせるようなUGCコンテンツを作成したいです。

エムグラムの場合、結果ページのURLが https://mgram.me/ja/share/xxxxxxxxxxxxxxxxxxx 
という形式になっており、xxxxxxxxxxxxxxxxxxxの部分が
結果によって変わるランダムな文字列になっていました。

このようなURLはどのように生成しているのでしょうか?

▼例
・入力した内容をDBに保存し、DBから返すIDのようなものをURLに振って結果ページを表示させる?
・入力内容をパラメーターで渡し、パラメーター部分を短縮処理させたうえで結果ページのURLにしている?

0

2Answer

即席でつくった

成果物

無題.png

使用ライブラリ周り

  • FireStore

FireStore ルール

rules_version = '2';
service cloud.firestore {
  function isValidSchema(info) {
    return info.size() == 1
      && 'message' in info && info.message is string
  }

  function isValidData(info) {
    return 1 <= info.message.size() && info.message.size() <= 150
  }

  match /databases/{database}/documents {
    match /info/{infoId} {
      allow get: if true;
    }

    match /info/{infoId} {
      allow create: if isValidData(request.resource.data)
                    && isValidSchema(request.resource.data);
    }
  }
}

ソース

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css"
      integrity="sha384-r4NyP46KrjDleawBgD5tp8Y7UzmLA05oM1iAEQ17CSuDqnUK2+k9luXQOfXJCJ4I"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <h2>共有したいメッセージを入力</h2>
    <div class="form-group">
      <textarea
        class="form-control"
        rows="3"
        placeholder="メッセージ150字以内"
        maxlength="150"
        style="width: 50%"
        id="textarea"
      ></textarea>
      <button type="button" class="btn btn-primary" id="button">投稿</button>
    </div>

    <script
      src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js"
      integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/"
      crossorigin="anonymous"
    ></script>
    <script src="https://www.gstatic.com/firebasejs/7.8.1/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/7.8.1/firebase-firestore.js"></script>
    <script src="js/index.js"></script>
  </body>
</html>
sub.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css"
      integrity="sha384-r4NyP46KrjDleawBgD5tp8Y7UzmLA05oM1iAEQ17CSuDqnUK2+k9luXQOfXJCJ4I"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <div id="info" style="display: none">
      <a href="" id="back">もどる</a><br /><br />
      <a href=""  target="_blank" rel="noopener noreferrer" id="tweet"
        >ツイートする</a
      ><br /><br />

      <div class="form-group">
        <textarea
          class="form-control"
          rows="3"
          maxlength="150"
          style="width: 300px"
          id="textarea"
          readonly
        ></textarea>
      </div>
    </div>

    <div id="loading">
      <div
        class="position-absolute h-100 w-100 m-0 d-flex align-items-center justify-content-center"
      >
        <div class="spinner-border text-primary" role="status">
          <span class="sr-only">Loading...</span>
        </div>
      </div>
    </div>

    <script
      src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js"
      integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/"
      crossorigin="anonymous"
    ></script>
    <script src="https://www.gstatic.com/firebasejs/7.8.1/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/7.8.1/firebase-firestore.js"></script>
    <script src="js/sub.js"></script>
  </body>
</html>
js/index.js
const dir = 'rarala2020_2';

const firebaseConfig = {
  apiKey: 'AIzaSyB8olQXWPvTu9nehgWClS7PZzRLKUrqEtw',
  authDomain: 'fddfd-f7b92.firebaseapp.com',
  databaseURL: 'https://fddfd-f7b92.firebaseio.com',
  projectId: 'fddfd-f7b92',
  storageBucket: 'fddfd-f7b92.appspot.com',
  messagingSenderId: '301190636009',
  appId: '1:301190636009:web:3ee8471f31d4aa64fcab78',
  measurementId: 'G-854P4MZ60D',
};

firebase.initializeApp(firebaseConfig);

document.getElementById('button').onclick = async () => {
  try {
    const _message = document.getElementById('textarea').value;

    if (_message.length === 0) {
      alert('未入力です');
      return;
    }

    document.getElementById('textarea').value = '';

    // データ挿入
    const _ret = await firebase.firestore().collection('info').add({
      message: _message,
    });

    location.href = `/${dir}/sub.html?q=${_ret.id}`;
  } catch (error) {
    console.log(error);
    alert('エラー発生');
  }
};
js/sub.js
const dir = 'rarala2020_2';

const firebaseConfig = {
  apiKey: 'AIzaSyB8olQXWPvTu9nehgWClS7PZzRLKUrqEtw',
  authDomain: 'fddfd-f7b92.firebaseapp.com',
  databaseURL: 'https://fddfd-f7b92.firebaseio.com',
  projectId: 'fddfd-f7b92',
  storageBucket: 'fddfd-f7b92.appspot.com',
  messagingSenderId: '301190636009',
  appId: '1:301190636009:web:3ee8471f31d4aa64fcab78',
  measurementId: 'G-854P4MZ60D',
};

firebase.initializeApp(firebaseConfig);

// https://so-zou.jp/web-app/tech/programming/javascript/sample/get.htm
// getパラメータ取得
function GetQueryString() {
  var result = {};
  if (1 < window.location.search.length) {
    // 最初の1文字 (?記号) を除いた文字列を取得する
    var query = window.location.search.substring(1);

    // クエリの区切り記号 (&) で文字列を配列に分割する
    var parameters = query.split('&');

    for (var i = 0; i < parameters.length; i++) {
      // パラメータ名とパラメータ値に分割する
      var element = parameters[i].split('=');

      var paramName = decodeURIComponent(element[0]);
      var paramValue = decodeURIComponent(element[1]);

      // パラメータ名をキーとして連想配列に追加する
      result[paramName] = paramValue;
    }
  }
  return result;
}

// https://qiita.com/mas0061/items/c2e9cd0d27e09448d28e
// サニタイズ
const sanitaize = {
  encode: function (str) {
    return str
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  },
};

const main = async () => {
  const params = GetQueryString();

  if (params['q'] === undefined) {
    location.href = `/${dir}`;
    return;
  }

  const _ret = await firebase
    .firestore()
    .collection('info')
    .doc(params['q'])
    .get();

  if (!_ret.exists) {
    location.href = `/${dir}`;
    return;
  }

  const _data = _ret.data();
  document.getElementById('textarea').value = sanitaize.encode(
    _data['message']
  );

  document.getElementById(
    'tweet'
  ).href = `http://twitter.com/share?url=${location.href.replace(
    location.search,
    ''
  )}${encodeURIComponent(location.search)}`;

  document.getElementById('loading').style.display = 'none';
  document.getElementById('info').style.display = 'block';

  document.getElementById('back').href = `/${dir}`;
};

main();
1Like

Comments

  1. @rarala2020

    Questioner

    サンプルコードありがとうございます(><)jsで作成いただいていたり、コメントや参考URLも記述いただいていて初心者の私でも分かりやすそうです。この機能だけ切り出すならFirebaseも使い勝手が良さそうなのでコードを参考にさせていただきます!

おそらく前者の

入力した内容をDBに保存し、DBから返すIDのようなものをURLに振って結果ページを表示させる?

の方ではないかと思います。
実際エムグラムがどうやってるかは分かりませんが、通常のWebサービスはほとんどこっちなので。

後者の

入力内容をパラメーターで渡し、パラメーター部分を短縮処理させたうえで結果ページのURLにしている?

でやればクライアント側の実装だけで済むので楽そうですが、ユーザー名が後から変更できない、OGP画像のURLだけはSSRしなければいけない、などの問題が出てくるのであまりやってるところは見ません。
(結局サーバー側実装したりDB使うなら前者の方が楽なので)

1Like

Comments

  1. @rarala2020

    Questioner

    なるほど!大変勉強になります。前者で対応する場合、具体的な実装方法(どのようにURLに渡すの?等)が分からず調べながら対応していくことになりそうなのですが、どのように検索すればこの実装の参考情報にたどり着けそうでしょうか…?( ; ; )
  2. ユーザーの入力したデータをDBに保存してそのデータを表示するというのは、Webアプリケーションとしてはかなり一般的な構造のためWebアプリケーションの勉強をしていけば必ずたどり着けるはずです。
    (検索するなら「投稿機能 <使いたいプログラミング言語 or Webフレームワーク名>」とかでしょうか)

    「どのようにURLに渡す」かについてですが、かなり簡単に言うと
    1. 「 http://example.com/item/<ID> 」のようにIDに紐づいたデータが表示できるURLを用意しておく。(例えば http://example.com/item/0a93c881 にアクセスするとID:0a93c881に紐づくデータが見れるような)
    2. サーバーにデータが投稿されたら、IDを生成してデータと一緒にDBに保存する。
    3. ID付きのURL( http://example.com/item/0a93c881 )にリダイレクトしたり、JavaScriptで遷移させたりしてそのURLをユーザーに表示する。
    といった感じでしょうか。

Your answer might help someone💌