突然ですが最近、就職活動を始めました。何かアピールできるものはないか…と考えた時、ふとSVGとPHPで帳票生成をされている方を思い出しました。これで履歴書を作ったら一石二鳥なのでは? と思い、さっそくやってみることにしました。
履歴書テンプレートをSVGで作成する
厚生労働省ウェブサイトから、履歴書をPDFでダウンロードします。
ダウンロードしたPDFをIllustratorで開き、テンプレートを作成します。FigmaやXDで作業したい場合は、一旦SVGで書き出したものを読み込みます。
テンプレートができたらこのように、変数にしたい部分を{name}
のように記入します。職歴など配列を流し込む場合は、{career[0]?.y}
のような感じにします。
そして、変数部分はアウトライン化せずテキストのままで書き出します。書き出したら、好みでSVGOにかけるなどして整理してもいいでしょう。下のリンクはGUIでSVGを軽量化するツールです。
SVGデータをもとにコンポーネントを作成する
SVGデータができあがったら、その内容でコンポーネントを作ります。SVGコードをペーストする際にはstroke-width
→strokeWidth
など、Reactに合わせて細かいところを修正しておきます。
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);
}
}
また、設定ファイルも作っておきます。ここに名前や連絡先など、流し込むデータを記入しておきます。
export default {
date: {
y: 2024,
m: 9,
d: 20,
},
name: '山田 太郎',
name_kana: 'やまだ たろう',
// 後略
}
最後に、テンプレートコンポーネントと設定ファイルを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
を指定してください。
<text>
{pr &&
splitText(pr)?.map((text, i) => (
<tspan x="41" y={542.69 + 15 * i} key={i}>
{text}
</tspan>
))}
</text>
顔写真の挿入
次に、顔写真を挿入します。/public
ディレクトリに任意の画像を格納し、呼び出します。本当はここもSVGに埋め込みたかったのですが、img要素をabsoluteで乗せるほうが楽だったので横着してしまいました…。
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になります。
おわりに
難しいのでは…と思いましたが、ちゃんと実装できてよかったです。SVGもソースコードなので、位置を調整したりフォントをいじったりするためにFigmaへ戻る必要がなかったので楽だな〜というのが今回の発見です。