Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
PHP製のPodCast配信用WebアプリをReact+Next.jsなSSGで作り直してみた話
Search
Kaz Watanabe
March 26, 2021
Programming
720
3
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
PHP製のPodCast配信用WebアプリをReact+Next.jsなSSGで作り直してみた話
Kaz Watanabe
March 26, 2021
More Decks by Kaz Watanabe
See All by Kaz Watanabe
開発エンジニアが取り組む DevSecOps ~ GitHub Enterprise × Azure での実践~
kaz29
0
52
Greenは本当にGreenか? - B/GデプロイとAPI自動テストで安心デプロイ
kaz29
1
210
CI/CD/IaC 久々に0から環境を作ったらこうなりました
kaz29
1
510
開発エンジニアが実践するDevSecOps
kaz29
0
180
PHPCon福岡2024-Azureもなかなかいいですよ.pdf
kaz29
2
400
Azure Container Apps + Bicep 〜 こんな感じで運用しています
kaz29
3
1.3k
20220908_フロントエンドパフォーマンス改善.pdf
kaz29
2
200
バックエンドエンジニアの私がお勧めする SPAフロントエンド開発環境
kaz29
6
6.3k
201909-PHPCon北海道-PHPでCI_CD.pdf
kaz29
0
4.3k
Other Decks in Programming
See All in Programming
Atomic Design, Enforced: Scaling Mobile Design Systems (next.app devCon / droidCon Berlin 2026)
steliosf
PRO
0
170
R8 の設定見直しでアプリサイズ削減
andpad
0
220
Apple Intelligence を用いた個人情報誤送信防止、及びユーザーリクエスト体験の改善について
yukiny
0
350
Herb in Rails 8.2: Your ERB views, now HTML-aware @ Kaigi on Rails 2026, Tokyo, Japan
marcoroth
2
290
Railsのためのパスキー実践入門 〜マルチテナントSaaSでの意思決定〜
utakaha
3
780
contenteditable と日本語入力に向き合う
colorful12
0
200
AIで開発が速くなった先で、開発チームは何を目指すか
shihochan
1
1.6k
AI主導のプログラミング学習アプリを作ってみた
econanringo
0
180
GemmaをJevのように使ってみる / Use Gemma like Jev
kishida
5
800
全ての永続トークンを、生まれる前に消し去りたい!
sylph01
2
1k
設計次第でAIコードの読む量は減らせる / designing-for-code-reading
minodriven
35
17k
sbt 2.0: takeoff
eed3si9n
0
160
Featured
See All Featured
Digital Ethics as a Driver of Design Innovation
axbom
PRO
1
460
How to build an LLM SEO readiness audit: a practical framework
nmsamuel
2
950
Keith and Marios Guide to Fast Websites
keithpitt
413
23k
Darren the Foodie - Storyboard
khoart
PRO
4
4k
The MySQL Ecosystem @ GitHub 2015
samlambert
251
13k
DevOps and Value Stream Thinking: Enabling flow, efficiency and business value
helenjbeal
2
410
Facilitating Awesome Meetings
lara
57
7.2k
Deep Space Network (abreviated)
tonyrice
0
380
Building Better People: How to give real-time feedback that sticks.
wjessup
371
20k
30 Presentation Tips
portentint
PRO
1
430
Rails Girls Zürich Keynote
gr2m
96
14k
Noah Learner - AI + Me: how we built a GSC Bulk Export data pipeline
techseoconnect
PRO
0
460
Transcript
Θͨͳ!LB[@ 1)1ͷ1PE$BTU৴༻8FCΞϓϦ Λ3FBDU /FYUKTͳ44(Ͱ࡞Γͯ͠ Έͨ !1)1FS,BJHJ
8)0 ลҰ (Θͨͳ ͔ͣͻΖ) @kaz_29 גࣜձࣾϋʔτϏʔπ ։ൃࣄۀ෦
גࣜձࣾϋʔτϏʔπ ຊۀMSPͷձࣾͰɺ ։ൃࣄۀΛ͍ͬͯ·͢ɻ IUUQTIFBSUCFBUTKQ
Agenda • PodCastͷΈ • v1(چ൛)ͷߏ • WHY? • v2Ͱ༻͢Δٕज़ཁૉ •
SPA / SSR / SSG ͱ? • v2ͷಈ࡞ڥ • ·ͱΊ
IC4","#"
Podcastͷ৴
Podcastͷ৴ w 344'FFE w গͳ͘ͱ݅ͷΤϐιʔυ w ΞʔτϫʔΫ IUUQTIFMQBQQMFDPNJUDQPEDBTUT@DPOOFDUJUDDC
Podcastͷ৴ IUUQTIFMQBQQMFDPNJUDQPEDBTUT@DPOOFDUJUDDC
v1ͷಈ࡞ڥ • PHP / Slim3 • neon • Docker •
Azure DevOps
v1ͷಈ࡞ڥ
v1ͷಈ࡞ڥ "[VSF8FC"QQGPS$POUBJOFST ίʔυΛQVTI ίϯςφΠϝʔδΛQVTI ίϯςφΠϝʔδΛQVMM 4UBHJOHTMPUΛߋ৽ຊ൪ʹEFQMPZ "[VSF%FW0QT 3FQPT1JQFMJOFT "[VSF$POUBJOFS3FHJTUPSZ αΠτΛ֬ೝ
Իσʔλ "[VSF#MPC4UPSBHF
ಛʹͳ͘ ৴Ͱ͖͍ͯΔ͕…
React SSGԽ͍ͨ͠ʂ
WHY? • ӡӦͷखؒΛͳΔ͘ݮΒ͍ͨ͠ .neonฤू͢Δͷ໘😅 • ৴ͷίετ͍ํ͕ྑ͍ SSGԽ͢Δͱ੩తͳϑΝΠϧͷΈͷ৴ͳͷͰ͍ܰ • ࣄͷҊ݅ͰɺશͳSSGͳͦ͞͏ͳͷͰݟΛಘ͍ͨ ં֯ͳͷͰҰൠʹެ։Ͱ͖ΔαΠτͰࢼ͍ͨ͠
SPA / SSR / SSG
SPA (Single Page Application / Client Side Rendering) • ϝϦοτ
• JSͷϑϨʔϜϫʔΫͰϦονͳUXΛఏڙͰ͖Δ • ϖʔδભҠ͕ૣ͍(࡞ΓʹΑΔ͕…) • σϝϦοτ • ॳظද͕͍ࣔ • SEOతʹෆར(࠷ۙͦ͏Ͱͳ͍…?)
SSR (Server Side Rendering) • ϝϦοτ • දࣔ·Ͱͷ͕࣌ؒॖͰ͖Δ αʔόͰϨϯμϦϯά͢ΔͨΊɻαʔόʔଆʹෛՙ͕͔͔Δɻ •
SEOతʹ༗ར(࠷ۙͦ͏Ͱͳ͍…?) • σϝϦοτ • nodeͷαʔό͕ඞཁ • ཧ͕໘
SSG (Server Side Generator) • ϝϦοτ • ੩తͳαΠτͳͷͰ͍ܰ • Webαʔό͚ͩͰ৴Ͱ͖Δ
• σϝϦοτ • ಈతͳϖʔδ͕ଟ͍αΠτʹ͔ͳ͍ • ߋ৽ස͕ߴ͍αΠτʹ͔ͳ͍ • ϖʔδ͕૿͑ΔͱϏϧυʹ͕͔͔࣌ؒΔ
SSG + SPA • SSG ͱ SPAͷϋΠϒϦου(?) • SSRͳ͠ͰׂΓΔ ͦͦϩάΠϯ͕ඞཁͩ͠SEOͱ͔…
• LPผͰ࡞ • ߋ৽ϖʔδ͕ଟͯ͘େৎ • WebαʔόͷΈͰ৴Մೳ(mod_rewriteతͳػೳඞਢ)
SSG Podcastͷ৴ʹ࠷దʂ
༻͢Δٕज़ཁૉ • docker / docker compose • PHP / CakePHP
4.x • swagger-php • Next.js • Typescript • Storybook • PostgreSQL • Swagger UI • Github Actions • Azure Static Web Apps (PREVIEW)
CakePHP IUUQTDBLFQIQPSH
༻͢Δٕज़ཁૉ CakePHP • 4ܥ͕ग़ͯ1Ҏ্ܦաͯ͠ɺ࠷৽4.2.3 • ܧଓͯ͠৭ʑվળ͞Ε͍ͯΔ • ·ͩ·ͩݩؾͰ͢Αʂ
Swagger UI IUUQTTXBHHFSJPUPPMTTXBHHFSVJ
༻͢Δٕज़ཁૉ Swagger UI /** * Index method * * @OA\Get(
* path="/api/articles.json", * tags={"COMMON"}, * summary="هࣄҰཡऔಘAPI", * description="هࣄҰཡऔಘAPI", * @OA\Parameter( * name="page", * in="query", * @OA\Schema( * type="integer", * ), * description="ϖʔδࢦఆ", * ), * @OA\Response( * response=200, * description="successful operation", * @OA\JsonContent( * @OA\Property( * property="success", * type="boolean", * default=true, * ), * @OA\Property( * property="data", * type="array", * @OA\Items(ref="#/components/schemas/Article"), * ), * ), * ), * ) IUUQTTXBHHFSJPUPPMTTXBHHFSVJ
༻͢Δٕज़ཁૉ Swagger UI IUUQTTXBHHFSJPUPPMTTXBHHFSVJ
༻͢Δٕज़ཁૉ Swagger UI IUUQTTXBHHFSJPUPPMTTXBHHFSVJ
ٕज़ཁૉ React • FacebookۘͷJavaScriptϑϨʔϜϫʔΫ • Angularͱ͔Vueͱ͔ͱΑ͘ൺΒΕΔ • ࠃͰVue͕ਓؾ? • ࠓճ͍׳ΕͨReactͰ͢͢Ί·͢
Storybook IUUQTTUPSZCPPLKTPSH
༻͢Δٕज़ཁૉ Storybook import React from 'react' import { Meta }
from '@storybook/react/types-6-0' import Presentational from '.' import { Article } from '~/types' import { articles } from '~/__fixture__/articles' export default { title: 'components/views/Admin/Articles' } as Meta const onSelect = (article: Article) => { console.info(article) } const onClick = () => console.info('onClick') const onSync = () => console.info('onSync') export const Default = () => <Presentational loading={false} articles={articles} onSelect={onSelect} onClick={onClick} /> IUUQTTUPSZCPPLKTPSH
༻͢Δٕज़ཁૉ Storybook IUUQTTUPSZCPPLKTPSH
Next.js IUUQTOFYUKTPSH
༻͢Δٕज़ཁૉ Next.js • RectϑϩϯτΤϯυ։ൃ༻ͷWebϑϨʔϜϫʔΫ • ॳظঢ়ଶͰSSRʹରԠ͍ͯ͠Δ Universal Javascript (Isomorphic JavaScript)
ରԠ • SSGʹରԠ͍ͯ͠Δ v9.3(2020/3)ͰSSGରԠͨ͠ • Կઃఆ͠ͳ͍ͰΑ͠ͳʹಈ͘ ຊՈ `No config needed.` Λᨳ͍ͬͯΔ ͕ɺ৭ʑઃఆͰ͖·͢ IUUQTOFYUKTPSH
Next.jsͷSSG
SSGͷղઆͷલʹ… Next.jsͷDynamic Routing • pages/article/[id].tsx {ม໊}.[js or tsx]ͳϑΝΠϧΛ༻ҙ͢Δͱ… • http://example.com/article/1
͜Μͳײ͡ͷΞΫηεΛࣗಈͰroutingͯ͘͠ΕΔػೳ ctx.query.id ͰΞΫηεՄೳ • pages/article/[id]/edit.tsx σΟϨΫτϦΛಈతʹࢦఆ͢Δ͜ͱՄೳ
SPA / SSRͷ࣌ http://example.com/article/{id} import React from 'react' import {
NextPage, NextPageContext } from 'next' type Props = { article: Article } const HogePage: NextPage<Props> = ({ article, }) => ( return <ArticleView article={article} /> ) ArticlePage.getInitialProps = async ({ query }: NextPageContext) => { const id = Number(query.id) const res = await fetch(`${process.env.NEXT_PUBLIC_API_ENDPOINT}/article/${id}.json`) const response = await res.json() return { article: response.data } } export default ArticlePage - URLʹΞΫηε͞Εͨ߹ʹαʔόʔαΠυͰಈ͘ - ϒϥβͰભҠͨ͠߹ʹΫϥΠΞϯταΠυͰಈ͘
SSG http://example.com/article/{id} type Props = { article: Article } const
ArticlePage: NextPage<Props> = ({ article, }) => { return <ArticleView article={article} /> } export const getStaticPaths: GetStaticPaths = async () => { const res = await fetch(`${process.env.NEXT_PUBLIC_API_ENDPOINT}/articles.json`) const response = await res.json() const paths: any[] = [] for (const article of response.data) { paths.push({ params: { id: String(article.id) } }) } return { paths, fallback: false, } } export const getStaticProps: GetStaticProps = async (context: GetStaticPropsContext) => { const id = context.params && context.params.id if (!id) { return { notFound: true } } const res = await fetch(`${process.env.NEXT_PUBLIC_API_ENDPOINT}/articles/${id}.json`) const response = await res.json() return { props: { article: response.data } } } export default ArticlePage ଘࡏ͢ΔϖʔδͷҰཡΛऔಘ ݸผͷϖʔδͷใΛऔಘ (getInitialPropsʹ૬)
SSG Ϗϧυ݁Ռ
SSG http://example.com/article/{id} <IfModule mod_rewrite.c> RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteRule
^([0-9]+)$ article/$1.html [L] </IfModule> ৴ڥͰ͜Μͳײ͡ͷઃఆ͕ඞཁ http://example.com/1 => http://example.com/article/1.html
͓·͚ (SSG + SPAͷ߹) http://example.com/article/{id} import { NextPage } from
'next' import React from 'react' import AdminLayout from '~/layouts/Admin/index' import AdminArticleView from '~/components/views/Admin/Article' import { useRouter } from 'next/router' type Props = React.ComponentProps<typeof AdminArticleView> const AdminArticlesPage: NextPage = () => { const router = useRouter() const props: Props = { article_id: Number(router.query.id), } return ( <AdminLayout> <AdminArticleView {...props} /> </AdminLayout> ) } export default AdminArticlesPage ApacheͳͲͷWebαʔόͰ৴͢Δ߹ɺ SSRͰͳ͍ͷͰαʔόαΠυͰgetInitialPropsવಈ͔ͳ͍ɻ ࣗલͰDynamic RoutingͷύϥϝʔλΛऔಘ͢Δඞཁ͕͋Δ
༻͢Δٕज़ཁૉ Azure Static Web Apps (PREVIEW) • ੩తαΠτͷϗεςΟϯάαʔϏε • GitHub
ActionsʹΑΔ Build & Deploy (GitHub࿈ܞඞਢ) • ΧελϜυϝΠϯରԠɺSSLূ໌ॻ(ແྉ) ΧελϜυϝΠϯͷઃఆΛ͢ΔͱɺࣗಈతʹSSLূ໌ॻ࡞͞ΕΔ Zone Apex(Naked Domainʣݱঢ়ඇରԠ (CloudflareͬͨΓ͢ΕରԠͰ͖Δ) • ϓϨϏϡʔػೳ PRΛ࡞͢ΔͱɺϓϨϏϡʔ൛͕ࣗಈσϓϩΠɻϚʔδ͢ΔͱϓϨϏϡʔ൛ࣗಈআ͞Εຊ൪өʹөɻ • ΞϓϦΤϯδχΞʹ͍͍͢ • ࠓͷͱ͜Ζແྉ • ϩʔΧϧڥͰ࣮ߦՄೳͳػೳ(static-web-apps-cli)͕ϦϦʔε͞Εͨ (2021/3/5 new!) • https://github.com/Azure/static-web-apps-cli IUUQTB[VSFNJDSPTPGUDPNKBKQTFSWJDFTBQQTFSWJDFTUBUJD
v2ͷಈ࡞ڥ
v2ͷಈ࡞ڥ "[VSF4UBUJD8FC"QQT (JUIVC"DUJPOT "[VSF4UBUJD8FC"QQT 13Λ࡞ (JUIVC"DUJPOT CVJMEΛ࣮ߦ σϓϩΠ ։ൃɾςετ 13
هࣄσʔλ Λಉظ هࣄσʔλΛऔಘ Իσʔλ "[VSF#MPC4UPSBHF
ಈ͔Μ😱
v2ͷಈ࡞ڥ "[VSF4UBUJD8FC"QQT (JUIVC"DUJPOT • ΤϯυϙΠϯτ͓ͦΒ͘App Service (Windows)? • ੈք֤ͷཧతʹࢄͨ͠ϙΠϯτ͔Βఏڙ͞ΕΔ https://docs.microsoft.com/ja-jp/azure/static-web-apps/overview
• Azure Traffic managerܦ༝ͰΞΫηε͞ΕΔ EJH99999:::::;;;;;;;;;B[VSFTUBUJDBQQTOFU ʜ 99999:::::;;;;;;;;;B[VSFTUBUJDBQQTOFU*/" "/48&34&$5*0/ 99999:::::;;;;;;;;;B[VSFTUBUJDBQQTOFU*/$/".&B[VSFTUBUJDBQQTUSB⒏DNBOBHFSOFU B[VSFTUBUJDBQQTUSB⒏DNBOBHFSOFU*/$/".&NTIBILTUBUJDTJUFTQSPEFBTUBTJBQB[VSFXFCTJUFTOFU ʜ ͠Μࡶه"QQ4FSWJDF4UBUJD8FC"QQTͷΈΛ୳Δʢඇެࣜʣ IUUQTCMPHTIJCBZBOKQFOUSZ ඇެࣜͳใͰ͢
mod_rewrite͑Μ😱 Կ͔ผͷํ๏ͳ͍ͷ͔ʜ
v2ͷಈ࡞ڥ "[VSF4UBUJD8FC"QQT (JUIVC"DUJPOT • routes.jsonͰ੍ޚͰ͖Δ ͨͩ͠ਖ਼نදݱͱ͔ͰࢦఆͰ͖ͳ͍ͷͰॻ͖͍͑ͨϧʔτΛશͯهड़͢Δඞཁ͕͋Δ "[VSF4UBUJD8FC"QQTϓϨϏϡʔͰͷϧʔτ IUUQTEPDTNJDSPTPGUDPNKBKQB[VSFTUBUJDXFCBQQTSPVUFT \ SPVUFT<
\ SPVUF TFSWFBSUJDMFIUNM ^ > ^ ͜ΕΛΤϐιʔυهࡌ͠ͳ͍ͱ͍͔Μ…
v2ͷಈ࡞ڥ "[VSF4UBUJD8FC"QQT (JUIVC"DUJPOT • Ϗϧυ࣌ʹҰཡϖʔδͷgetStaticPropsͰࣗಈੜ const exportRoutes = (articles: Article[])
=> { const routes = { routes: [] as any, platformErrorOverrides: [ { errorType: 'NotFound', serve: '/404.html', }, ], mimeTypes: { xml: 'application/xml; charset=UTF-8', }, } for (const article of articles) { routes.routes.push({ route: `/${article.id}`, serve: `/article/${article.id}.html`, }) } fs.writeFileSync('./public/routes.json', JSON.stringify(routes)) } export const getStaticProps: GetStaticProps = async (_) => { firebaseInit() const db = firebase.firestore() const ref = db.collection('articles') const articles: Article[] = [] try { const query = await ref.orderBy('date', 'desc').get() query.forEach(doc => { articles.push(doc.data() as Article) }) if (process.env.BUILD === '1') { exportRoutes(articles) } } catch (error) { console.error(error) } return { props: { articles, url: process.env.NEXT_PUBLIC_WEB_ENDPOINT } } }
V2 https://heartbeats.jp/hbsakaba/ #hbsakaba
·ͱΊ
·ͱΊ • SSGศར • SSR / SSG / SSG+SPA దࡐదॴͰ͍͚Δͱྑ͍
• ϑϩϯτͷ։ൃָ͍͠Αʂ • ͪΖΜۤ࿑͋Δ͚ͲɺόοΫΤϯυಉ͡😅 • ࣗͰ੍ޚͰ͖ΔΞϓϦΛ࣋ͬͯΔͱ৭ʑ࣮ݧͰ͖ͯศར Enjoy!
͓͠·͍