はなゐろぐ

主に技術関係の覚え書きです。

Next.jsとSVGで履歴書を作る

2024年9月20日

突然ですが最近、就職活動を始めました。何かアピールできるものはないか…と考えた時、ふとSVGとPHPで帳票生成をされている方を思い出しました。これで履歴書を作ったら一石二鳥なのでは? と思い、さっそくやってみることにしました。

ついに、Webアプリでの帳票印刷のベストプラクティスを編み出しました
ついに、Webアプリでの帳票印刷のベストプラクティスを編み出しました favicon https://zenn.dev/ttskch/articles/1f1572cfd2e375
ついに、Webアプリでの帳票印刷のベストプラクティスを編み出しました

履歴書テンプレートをSVGで作成する

厚生労働省ウェブサイトから、履歴書をPDFでダウンロードします。

新たな履歴書の様式例の作成について
~「様式例」を参考にして、公正な採用選考をお願いします~
新たな履歴書の様式例の作成について favicon https://www.mhlw.go.jp/stf/newpage_kouseisaiyou030416.html

ダウンロードしたPDFをIllustratorで開き、テンプレートを作成します。FigmaやXDで作業したい場合は、一旦SVGで書き出したものを読み込みます。

テンプレートができたらこのように、変数にしたい部分を{name}のように記入します。職歴など配列を流し込む場合は、{career[0]?.y}のような感じにします。

Image from Gyazo

そして、変数部分はアウトライン化せずテキストのままで書き出します。書き出したら、好みでSVGOにかけるなどして整理してもいいでしょう。下のリンクはGUIでSVGを軽量化するツールです。

SVGOMG - SVGO's Missing GUI
Easy & visual compression of SVG images.
SVGOMG - SVGO's Missing GUI favicon https://jakearchibald.github.io/svgomg/
SVGOMG - SVGO's Missing GUI

SVGデータをもとにコンポーネントを作成する

SVGデータができあがったら、その内容でコンポーネントを作ります。SVGコードをペーストする際にはstroke-widthstrokeWidthなど、Reactに合わせて細かいところを修正しておきます。

Forms.tsx
import React, { FC } from 'react'

type Props = {
  date?: {
    y: number
    m: number
    d: number
  }
  name?: string
  name_kana?: string
  birthday?: {
    y: number
    m: number
    d: number
  }
  age?: number
  sex?: string
  postalcode?: string
  address?: string
  address_kana?: string
  phone?: string
  career?: {
    y: number
    m: number
    subject: string
  }[]
  license?: {
    y: number
    m: number
    subject: string
  }[]
  pr?: string
  note?: string
}

export const Forms: FC<Props> = ({
  date,
  name,
  name_kana,
  birthday,
  age,
  sex,
  postalcode,
  address,
  address_kana,
  phone,
  career,
  license,
  pr,
  note,
}: Props) => {
  return (
    <div className="forms">
      // ここに書き出したSVGコードをペースト
    </div>
  )
}

CSSは上でリンクした記事を参考に、こんな感じで書きます。page-break-afterを指定すると改ページで空のページができてしまうことがあるため、page-break-insideに変更しました。

@page {
  size: A4 portrait;
  margin: 0;
}

* {
  user-select: none;
}

body {
  width: 210mm;
  color-adjust: exact;
}

.forms {
  position: relative;
  width: 210mm;
  height: 290mm;
  page-break-inside: avoid;
}

.forms > svg {
  width: 100%;
  height: 100%;
}

.avatar {
  position: absolute;
  top: 4%;
  right: 8%;
  width: 13.94%;
  height: auto;
  aspect-ratio: 83/111;
  object-fit: cover;
}

@media screen {
  body {
    margin: 0 auto;
    background: #ccc;
  }

  .forms {
    margin-block: 5mm;
    background: #fff;
    box-shadow: 0 0.5mm 2mm rgba(0, 0, 0, 0.3);
  }
}

また、設定ファイルも作っておきます。ここに名前や連絡先など、流し込むデータを記入しておきます。

contents.ts
export default {
  date: {
    y: 2024,
    m: 9,
    d: 20,
  },
  name: '山田 太郎',
  name_kana: 'やまだ たろう',
  // 後略
}

最後に、テンプレートコンポーネントと設定ファイルをindex.tsxから読み込みます。

index.tsx
import Head from 'next/head'
import { Forms } from '@/components/Forms'
import contents from '@/contents'

export default function Home() {
  return (
    <>
      <Head>
        <title>履歴書</title>
      </Head>
      <Forms {...contents} />
    </>
  )

これで、設定が流し込まれて履歴書がブラウザに表示されれば成功です。

Noto Serif JPが出力されない問題

はじめはnext/fontでNoto Serif JPを読み込んでおり、ブラウザでは問題なく表示されたものの印刷してみると(プレビューの段階で)2バイト文字が出力されませんでした。Google Fontから配信されている他のウェブフォントは問題ありませんでした。理由を調べてみましたがいまいちよく分からず、結局headから読み込むことでことなきを得ました。

長文を折り返す

志望動機など、長文は折り返さずはみ出してしまいます。うまく枠の中におさめるため、一定の字数で分割してtspan要素を縦にずらしながら並べていきます。まずは、テキストを分割する関数を作ります。字数は好みで調節してください。

const splitText = (str) => {
  if (!str) return str

  return str
    // 改行で分割
    .split(/\n/)
    // その中で42文字ごとに分割
    .map((text) => {
      return [...text].reduce(
        (acc, c, i) =>
          i % 42 ? acc : [...acc, [...text].slice(i, i + 42).join('')],
        []
      )
    })
    // 最後に1行ずつ並べる
    .flat()
}

あとはテキストを折り返したいところで呼び出し、テキストを分割してtspan要素を出力していきます。y属性は1行目のy位置+行高さ*iを指定してください。

index.tsx
<text>
  {pr &&
    splitText(pr)?.map((text, i) => (
      <tspan x="41" y={542.69 + 15 * i} key={i}>
        {text}
      </tspan>
    ))}
</text>

顔写真の挿入

次に、顔写真を挿入します。/publicディレクトリに任意の画像を格納し、呼び出します。本当はここもSVGに埋め込みたかったのですが、img要素をabsoluteで乗せるほうが楽だったので横着してしまいました…。

index.tsx
import Image from 'next/image'

<div className="forms">
  <Image
    src="/avatar.jpg"
    width="600"
    height="800"
    alt=""
    className="avatar"
  />
  <svg></svg>
</div>

できあがり

最終的にこんな感じになります。ブラウザの印刷機能からPDFで保存するとちょうどA4サイズx2になります。

Image from Gyazo

おわりに

難しいのでは…と思いましたが、ちゃんと実装できてよかったです。SVGもソースコードなので、位置を調整したりフォントをいじったりするためにFigmaへ戻る必要がなかったので楽だな〜というのが今回の発見です。