Skip to content

Commit

Permalink
Environment: ページごとの OGP タグ値の注入ルールを各ルートディレクトリーで定義できるようにする (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
ThinaticSystem authored Jul 10, 2024
1 parent 24785cb commit df1d453
Show file tree
Hide file tree
Showing 19 changed files with 349 additions and 45 deletions.
2 changes: 1 addition & 1 deletion docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
## HTMLの動的生成
OGPタグの動的生成を実現するため、EJSテンプレートによるレンダリングをバックエンドに導入する。\
ViteはデフォルトではHTMLファイルを生成するため、生成されたHTMLファイルをベースにEJSファイルを生成する。\
バックエンドはEJSのレンダリングで使用できるいくつかのパラメータを提供する。\
バックエンドはEJSのレンダリングで使用できるいくつかのパラメータを提供する。
3 changes: 3 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?

# generate-template-html.js で生成
/index.html
5 changes: 3 additions & 2 deletions frontend/docs/regulations.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

ドキュメント: https://tanstack.com/router/latest/docs/framework/react/overview

- ページのファイルは`route.tsx``route.lazy.tsx`とし、`createFileRoute``createLazyFileRoute`を使って`Route`コンポーネントをエクスポートする
- lazyで使う場合はlazyを使う
- ページのファイルは`route.tsx`(lazyで使う場合は`route.lazy.tsx`も)とし\
`createFileRoute`(lazyで使う場合は`createLazyFileRoute`も)を使って`Route`コンポーネントをエクスポートする
- `createFileRoute`@`route.tsx`のオプションの`staticData`プロパティーでページのOpen Graph Protocol情報を定義する
- ルートを出力しないファイル・フォルダーに関してはファイル先頭に`-`を付ける(`-components/`)など
- 型定義ファイルを出力するために、`pnpm dev`のほかに`pnpm generate-routes`もしくは`pnpm watch-routes`コマンドを実行する
- `__root.tsx`はすべてのルートで適用されます
Expand Down
14 changes: 0 additions & 14 deletions frontend/index.html

This file was deleted.

4 changes: 3 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
"type": "module",
"scripts": {
"generate-routes": "tsr generate",
"generate-html-template": "pnpm exec tsx --tsconfig tsconfig.app.json --import=./scripts/ignore-assets-loader/register.js scripts/generate-template-html.js",
"watch-routes": "tsr watch",
"dev": "vite",
"build": "generate-routes && tsc -b && vite build && node ./scripts/generate-ejs.js",
"build": "pnpm run generate-routes && pnpm run generate-html-template && tsc -b && vite build && node ./scripts/generate-ejs.js",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "pnpm lint --fix",
Expand Down Expand Up @@ -38,6 +39,7 @@
"eslint-plugin-react-refresh": "^0.4.7",
"eslint-plugin-unused-imports": "^4.0.0",
"prettier": "^3.3.2",
"tsx": "^4.16.0",
"typescript": "^5.5.2",
"vite": "^5.3.1",
"vite-tsconfig-paths": "^4.3.2"
Expand Down
27 changes: 27 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/scripts/generate-ejs.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const distFile = process.cwd() + '/dist/index.ejs';
const source = fs.readFileSync(sourceFile, { encoding: 'utf8' });

// ejsのコメントを全てejs構文に置換
const dist = source.replace(/<!--[ \t]*(%.+?%)[ \t]*-->/g, '<$1>');
const dist = source.replace(/<!--[ \t]*(%.+?%)[ \t]*-->/gs, '<$1>');

// ejsファイルへ書き込み
fs.writeFileSync(distFile, dist);
143 changes: 143 additions & 0 deletions frontend/scripts/generate-template-html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// @ts-check

import fs from "node:fs";
import path from "node:path";
import process from "node:process";

import { routeTree } from "../src/routeTree.gen";
import { BACKEND_EJS_VARIABLE_KEYS } from "../src/staticDataRouteOption";

// Constants

/** 書き出すHTMLファイルのパス */
const distFile = path.resolve(process.cwd(), "index.html");

// Utilities

/**
* 半角スペースとそれに続く `|` がある行はそれらを取り除き\
* ない行は行ごと取り除く
*
* @param {string} text
*/
const trimMargin = (text) =>
text
.split("\n")
// 行頭が半角スペース (ない or 任意個数)と | ではない行を取り除く
.filter((line) => line.match(/^\s*\|/))
// 行頭の半角スペース (ない or 任意個数)と | を取り払う
.map((line) => line.replace(/^\s*\|/, ""))
.join("\n");

/**
* 全ての行にインデントを付与した文字列を返す
* @param {string} indentChar
* @param {number} indentLevel インデントの個数
* @param {string} sourceText
*/
const withIndent = (indentChar, indentLevel, sourceText) =>
sourceText
.split("\n")
.map((line) =>
line !== "" //
? indentChar.repeat(indentLevel) + line
: line,
)
.join("\n");

/** @typedef {import("../src/staticDataRouteOption").CustomStaticDataRouteOption} CustomStaticDataRouteOption */

/**
* @typedef {{
* isRoot: boolean;
* options: {
* path?: string;
* staticData: CustomStaticDataRouteOption;
* }
* children?: RouteTree[];
* }} RouteTree TanStack Router の `routeTree.children` の型が謎なのでとりあえず想定の型
*/

/**
* @typedef {{
* fullPath: string;
* openGraph: CustomStaticDataRouteOption["openGraph"];
* }} FlattenRoute
*/

/**
* `RouteTree[]` を `FlattenRoute` の一次元配列にフラット化したものを返す
*
* ネストされた `children` を収集する
* @param {Readonly<RouteTree[]>} routes
* @param {string} [pathPrefix='']
* @return {FlattenRoute[]}
*/
const getFlattenRoutes = (routes, pathPrefix = "") =>
routes.flatMap((route) => {
const fullPath = pathPrefix + route.options.path;
return [
{
fullPath: fullPath,
openGraph: route.options.staticData.openGraph,
},
// 子ルートを収集
...(route.children != null
? getFlattenRoutes(route.children, fullPath)
: []),
];
});
const flattenRoutes = getFlattenRoutes(
/** @type {RouteTree[]} */ (/** @type {unknown} */ (routeTree.children)),
);

const html = (() => {
const INDENT = " ";

const ogs = trimMargin(`
|<!-- % switch (${BACKEND_EJS_VARIABLE_KEYS.path}) {
${flattenRoutes
.map(
(route) => `
| case "${route.fullPath}": % -->
|<meta property="og:title" content="${route.openGraph.title}" />
|<meta property="og:type" content="${route.openGraph.type}" />
|<meta property="og:description" content="${route.openGraph.description}" />
|<meta property="og:url" content="<%- ${BACKEND_EJS_VARIABLE_KEYS.origin} + '${route.fullPath}' %>" />${
route.openGraph.imageUrl != null
? `|
|<meta property="og:image" content="${route.openGraph.imageUrl}" />`
: ""
}
| <!-- % break;`,
)
.join("\n")}
|} % -->
|
`);
return (
trimMargin(`
|<!doctype html>
|<html lang="ja">
|<head>
| <meta charset="UTF-8" />
| <link rel="icon" type="image/png" href="/logo192.png" />
| <meta name="viewport" content="width=device-width, initial-scale=1.0" />
| <title>Frost</title>
|
`) +
withIndent(INDENT, 1, ogs) +
trimMargin(`
|</head>
|<body>
| <div id="root"></div>
| <script type="module" src="/src/main.tsx"></script>
|</body>
|</html>
|
`)
);
})();

// htmlファイルへ書き込み
fs.writeFileSync(distFile, html);
5 changes: 5 additions & 0 deletions frontend/scripts/ignore-assets-loader/implement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// @ts-check

import { createMockResolver } from "../../../nodejs-mock-loader/src/index.js";

export const resolve = createMockResolver(["GIF"]);
6 changes: 6 additions & 0 deletions frontend/scripts/ignore-assets-loader/register.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// @ts-check

import { register } from "node:module";
import { pathToFileURL } from "node:url";

register(pathToFileURL("./scripts/ignore-assets-loader/implement.js"));
18 changes: 5 additions & 13 deletions frontend/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,15 @@

// This file is auto-generated by TanStack Router

import { createFileRoute } from '@tanstack/react-router'

// Import Routes

import { Route as rootRoute } from './routes/__root'
import { Route as LoginRouteImport } from './routes/login/route'
import { Route as RouteImport } from './routes/route'

// Create Virtual Routes

const LoginRouteLazyImport = createFileRoute('/login')()

// Create/Update Routes

const LoginRouteLazyRoute = LoginRouteLazyImport.update({
const LoginRouteRoute = LoginRouteImport.update({
path: '/login',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/login/route.lazy').then((d) => d.Route))
Expand All @@ -46,18 +41,15 @@ declare module '@tanstack/react-router' {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginRouteLazyImport
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRoute
}
}
}

// Create and export the route tree

export const routeTree = rootRoute.addChildren({
RouteRoute,
LoginRouteLazyRoute,
})
export const routeTree = rootRoute.addChildren({ RouteRoute, LoginRouteRoute })

/* prettier-ignore-end */

Expand All @@ -75,7 +67,7 @@ export const routeTree = rootRoute.addChildren({
"filePath": "route.tsx"
},
"/login": {
"filePath": "login/route.lazy.tsx"
"filePath": "login/route.tsx"
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createRootRoute, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";

import type { OpenGraphProtocolType } from "@/staticDataRouteOption";

import { Header } from "@/components/Header";

export const Route = createRootRoute({
Expand All @@ -11,4 +13,12 @@ export const Route = createRootRoute({
<TanStackRouterDevtools />
</>
),
staticData: {
// RootRouteのOGP情報は使用しないが型定義上何かしら入れる必要がある
openGraph: {
title: "Don't Care",
type: "Don't Care" as OpenGraphProtocolType,
description: "Don't Care",
},
},
});
16 changes: 3 additions & 13 deletions frontend/src/routes/login/route.lazy.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
import { Button } from "@mantine/core";
import { Link, createLazyFileRoute } from "@tanstack/react-router";
import { createLazyFileRoute } from "@tanstack/react-router";

import { Login } from "./route";

export const Route = createLazyFileRoute("/login")({
component: Login,
});

function Login() {
return (
<>
<p>Login Page</p>
<Link to="/">
<Button>Top</Button>
</Link>
</>
);
}
Loading

0 comments on commit df1d453

Please sign in to comment.