ãã®èšäºã¯ Static Site Generator Advent Calendar 2020 22 æ¥ç®ã®èšäºã§ãã
ã¯ããã«
Hugo ã®ãŠã§ããµã€ãã«çµã¿èŸŒã RSS ãªãŒããŒã TypeScript ã§éçºããŠã¿ãããšæã調æ»ãããšãããHugo ã®ææ°çã«ã¯ ESBuild ãçµã¿èŸŒãŸããŠããŠãéåžžã«æåã JavaScript ã®éçºç°å¢ããµããŒããããŠããããšãåãããŸããã æ¬èšäºã§ã¯çŽ¹ä»ããŠããŸããã Babel ãå©çšã§ããããã§ãã
ãŸããNPM ããã±ãŒãžãå©çšã§ãããããæ®æ®µã®ãŠã§ãéçºãšåæ§ã®æµãã§éçºãã§ããåçš®ã©ã€ãã©ãªãçšããéçºãéåžžã«æ¥œã§ããã ä»å㯠Hugo 㧠JavaScript éçºããæ¹æ³ã RSS ãªãŒããŒã®éçºãäŸã«äžããããã§åŸãç¥èŠã«ã€ããŠã亀ãã圢ã§èšäºãšããŠæ®ããŠããããšã«ããŸããã
ã¡ãªã¿ã«æ¬èšäºå
容㯠Hugo 㧠JavaScript éçºããæ¹æ³ã«çŠç¹ãçµã£ããã®ãªã®ã§ããããŠã§ããµã€ãã« RSS ãªãŒããŒãçµã¿èŸŒãããšã«çŠç¹ãçµã£ãŠèŠããæ¹ã¯ RSS ãªãŒããŒã Hugo ã® Data Templates ã§å®è£
ãã
ããèŠãŠããã ãããšããªã¹ã¹ã¡ããŸãã
Hugo 㧠JavaScript (React + TypeScript) ã®éçºç°å¢ãæŽãã
ãŸããTypeScript ã®ãã«ã㯠ESBuild ã«ä»»ããããšãã§ããããäœãè¡ãå¿ èŠã¯ãããŸããã ãã®ãã React éçºçšããã±ãŒãžã®ã€ã³ã¹ããŒã«ã®ã¿è¡ãã°å€§äžå€«ã§ãã
Hugo ãããžã§ã¯ãã®ã«ãŒããã£ã¬ã¯ããªã§äžèšã³ãã³ããå®è¡ããpackage.json
ãäœæããŠãããReact ã®éçºã«å¿
èŠãªããã±ãŒãžãã€ã³ã¹ããŒã«ããŸãã
npm init -y
npm install --save react react-dom
ç¡äºããã±ãŒãžã®ã€ã³ã¹ããŒã«ãå®äºããããæ©é TSX ãã¡ã€ã«ã assets/js/App.tsx
ã«äœæããŠããŸããŸãã
// assets/js/App.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
function App() {
return <>Hello React!</>;
}
ReactDOM.render(<App />, document.getElementById("react"));
äžèšã®ã³ãŒããèŠãŠãããã°åããéããã¬ã³ããªã³ã°å
ã« id
ã react
ã® DOM ããŒããæå®ããŠããŸãããã®ãã Hugo åŽã§è©²åœãã DOM ããŒããçšæããå¿
èŠããããŸãããã®éã® HTML ãã³ãã¬ãŒãã¯äžèšã«ãªããŸãã
<!-- ... -->
<!-- å©çšãããªãœãŒã¹ãæå®ãã -->
{{ with resources.Get "js/App.tsx" }}
<!-- id ã react ã® div èŠçŽ ãçšæãã -->
<div id="react"></div>
<!-- TSX ã ESBuild ã§ãã«ãããéã® Hugo ã®ãªãã·ã§ã³ãæå®ãã -->
{{ $options := dict "targetPath" "js/app.js" "minify" true "defines" (dict
"process.env.NODE_ENV" "\"development\"") }}
<!-- TSX ã®ãã«ãã Hugo ã®ãªãã·ã§ã³ã§æå®ããå
容ã§å®è¡ãã -->
{{ $js := resources.Get . | js.Build $options }}
<!-- äžå¿ SRI ãæå¹åããç¶æ
ã§ãã«ããã JS ãèªã¿èŸŒã -->
{{ $secureJS := $js | resources.Fingerprint "sha512" }}
<script
src="{{ $secureJS.Permalink }}"
integrity="{{ $secureJS.Data.Integrity }}"
></script>
{{ end }}
<!-- ... -->
ã¡ãªã¿ã« $options
ã§æå®ããŠãã ESBuild ã§ãã«ãæã«æå®å¯èœãªãªãã·ã§ã³ã¯ Hugo ã®å
¬åŒããŒãž ã«èšèŒãããŠããŸãã
äžèš HTML ã®èšè¿°ã RSS ãªãŒããŒãåã蟌ã¿ããããŒãžã«è¿œå ããŸãã ãã®ç¶æ ã§è©²åœããŒãžã«ã¢ã¯ã»ã¹ãããšäžèšã®ãããªè¡šç€ºã確èªã§ããã¯ãã§ãã
App.tsx ã§å®çŸ©ããå 容ãç»é¢ã«è¡šç€ºããã
ãã㧠React + TypeScript ã®éçºç°å¢ãæŽããŸããã
RSS ãªãŒããŒãå®è£ ãã
ããšã¯äžè¬ç㪠Web ããã³ããšã³ãéçºã®æµã㧠RSS ãªãŒããŒã®éçºãé²ããŠããã ãã§ãã
ãŠã§ããµã€ãã§èªã¿èŸŒã¿ãã RSS ãã£ãŒããæºåãã
RSS ãã£ãŒããå©çšããéã¯å¿ ãæäŸããŠãããµãŒãã¹ã®å©çšèŠçŽãã確èªãã ããã Qiita åã³ Zenn ã«ã€ããŠã¯å人å©çšãã€èªåã®æ å ±ã®ã¿ãæ±ãç¯å²å ã§ããã°å©çšãèš±å¯ãããŠããããã«èŠåããããŸããã1
äžæºåãšããŠãŠã§ããµã€ãã§èªã¿èŸŒã¿ãã RSS ãã£ãŒããäºåã«ããŠã³ããŒãããããã®ããããäœæããŸããããã㯠NPM ãå©çšããŠäœæããŠãããŸããNPM ãå°å ¥ããã®ã§ Hugo ã§å©çšããç°¡æãªããã㯠JavaScript ã§ãµã¯ããšäœæããŠãããŸãã
ãŸãã¯ã¹ã¯ãªããäœæã®éã«å¿ èŠãšãªãããã±ãŒãžãäºåã«ããã€ãã€ã³ã¹ããŒã«ããŸãã
# html ãããã¹ãå€æã«ããããã±ãŒãžãš RSS ãã£ãŒãã®ããŒãµãŒãã€ã³ã¹ããŒã«ãã
npm i -D --save html-to-text rss-parser
å®éã®ã³ãŒãã¯äžèšã«ãªããŸãããã¡ã€ã«åæ«å°Ÿã .mjs
ãªã®ã¯ Top-Level Await ã䜿çšãããããã§ãã
// scripts/update-rss.mjs
import { writeFileSync } from "fs";
import pkg from "html-to-text";
const { htmlToText } = pkg;
import Parser from "rss-parser";
const parser = new Parser();
// èªããã°ã§èªã¿èŸŒã¿ãã RSS ãã£ãŒãã®æ
å ±ãèšå®ãã
const rssFeed = {
Zenn: {
rss_url: "https://zenn.dev/nikaera/feed",
profile_url: "https://zenn.dev/nikaera",
},
Qiita: {
rss_url: "https://qiita.com/nikaera/feed.atom",
profile_url: "https://qiita.com/nikaera",
},
};
try {
const jsonFeed = {};
// RSS ãã£ãŒãå
ã® description ã 73åã§åãåãæ«å°Ÿã« ... ãä»äžããé¢æ°
const spliceContent = (content) => `${htmlToText(content).slice(0, 73)}...`;
// rssFeed å€æ°ã§å®çŸ©ãããŠãæ
å ±ãç¹°ãè¿ãåŠçãã
for (const [site, info] of Object.entries(rssFeed)) {
// RSS ãã£ãŒãã® URL ããå¿
èŠãªæ
å ±ãååŸãã
const feed = await parser.parseURL(info.rss_url);
// RSS ãã£ãŒãã«ç»é²ãããŠããé
ç®ã§å¿
èŠãªæ
å ±ã®ã¿ãååŸãã
const items = feed.items.map((i) => {
return {
title: i.title,
content: spliceContent(i.content),
url: i.link,
date: i.pubDate,
};
});
// ååŸå
容㯠jsonFeed ã«æ ŒçŽãã
const { rss_url, profile_url } = info;
jsonFeed[site] = { rss_url, profile_url, items };
}
// æåŸã« jsonFeed ã«æ ŒçŽãããå
容ã JSON æååãšã㊠static/rss.json ã«åºåãã
writeFileSync("./static/rss.json", JSON.stringify(jsonFeed));
} catch (err) {
console.error(err);
}
次㫠package.json
ã® scripts
ã«ç»é²ããŠã³ãã³ããšããŠå®è¡å¯èœã«ããŸãã
{
"scripts": {
"update-rss": "node ./scripts/update-rss.mjs"
}
}
ãã㧠npm run update-rss
ãå®è¡ããã°èªããã°ã§è¡šç€ºããéã«çšãã JSON ãã¡ã€ã«ãšã㊠RSS ãã£ãŒãã®å
容ã static/rss.json
ã«åºåã§ããŸãããŸããJSON ãã¡ã€ã«ã¯ static
ãã©ã«ãã«åºåããŠãããã http://localhost:1313/rss.json
ã§ã¢ã¯ã»ã¹ã§ããŸãã
npm run update-rss ãå®è¡ããŠåºåãã rss.json
http://localhost:1313/rss.json
ã«ã¢ã¯ã»ã¹ããŠåºåãã rss.json ãåç
§å¯èœãªããšã確èªãã
RSS ãªãŒããŒã React + TypeScript ã§å®è£ ãã
æºåãæŽã£ãã®ã§ãæ©é RSS ãªãŒããŒãäœæããŠãããŸãã
äžèšã¯ Hugo ã®ããŒãã® 1 ã€ã§ãã hugo-PaperMod ã® archives
ãã³ãã¬ãŒããå©çšããŠããŒãžã«åã蟌ãããšãæ³å®ãã RSS ãªãŒããŒã®ã³ãŒãã§ãã
// assets/js/Rss.tsx
import React, { useMemo, useState } from "react";
import * as superagent from "superagent";
const Rss = (props) => {
const [feed, setFeed] = useState({});
const { name } = props;
useMemo(() => {
(async () => {
try {
const res = await superagent.get("/rss.json");
setFeed(res.body[name]);
} catch (err) {
console.error(err);
}
})();
}, [name]);
if (!("items" in feed)) return null;
return (
<div className="archive-month">
<h3 className="archive-month-header">
<a href={feed.profile_url} target="_blank" rel="noopener noreferrer">
{name}
</a>{" "}
-{" "}
<a href={feed.rss_url} target="_blank" rel="noopener noreferrer">
RSS
</a>
</h3>
<div className="archive-posts">
{feed.items.map((item) => {
return (
<div className="archive-entry" key={item.url}>
<h3 className="archive-entry-title">{item.title}</h3>
<div className="archive-meta">
{item.date} - {item.content}
</div>
<a
className="entry-link"
href={item.url}
target="_blank"
rel="noopener noreferrer"
>
</a>
</div>
);
})}
</div>
</div>
);
};
export default Rss;
次㫠assets/js/App.tsx
㧠assets/js/Rss.tsx
ãèªã¿èŸŒã¿ç»é¢ã«è¡šç€ºã§ããããæ¹ä¿®ããŸãã
// assets/js/App.tsx
import Rss from "./Rss";
import * as React from "react";
import * as ReactDOM from "react-dom";
function App() {
return (
<>
<div class="archive-year">
<h2 class="archive-year-header">Tech ðŠŸ</h2>
<Rss name="Zenn" />
<Rss name="Qiita" />
</div>
</>
);
}
ReactDOM.render(<App />, document.getElementById("react"));
ãã㧠RSS ãªãŒããŒãåã蟌ãã ããŒãžãé²èŠ§ãããšäžèšã®ãããªç»é¢ã衚瀺ãããã¯ãã§ãã
hugo-PaperMod 㧠archives
ãã³ãã¬ãŒããçšã㊠RSS ãªãŒããŒã衚瀺ãããšãã®ç»é¢
ããä»ã® RSS ãã£ãŒããè¿œå ãããå Žå㯠scripts/update-rss.mjs
ã® rssFeed
å€æ°ã«æ
å ±ãè¿œå ããŠãApp.tsx
ã« <Rss name="<rssFeed å€æ°ã§å®çŸ©ãã RSS Feed å>" />
ãå®çŸ©ããããšã§å¯Ÿå¿ã§ããŸãã
RSS ãã£ãŒãã®å 容ãèªåã§æŽæ°ãã
npm run update-rss
ãæå
ã§å®è¡ã㊠static/rss.json
ãæŽæ°ããŠå
¬éããã°ãææ°ã® RSS ãã£ãŒãã®å
容ãããŒãžã«åæ ã§ããç¶æ
ã§ãããéœåºŠæåã§æŽæ°ããã®ã¯é¢åãªäœæ¥ã§ãã
ããã§ä»å㯠GitHub Actions ã® schedule
ãçšã㊠static/rss.json
ã®æŽæ°ãèªååããŸãã
GitHub Actions ã®ã¯ãŒã¯ãããŒãã¡ã€ã«ãäœæãã
å®éã®ã¯ãŒã¯ãããŒãã¡ã€ã«ã¯äžèšã«ãªããŸããschedule
ã®é
ç®ã§èšå®ããŠããå
容ãã¯ãŒã¯ãããŒã®å®è¡ã¹ã±ãžã¥ãŒã«ã«ãªããŸããä»åã¯åæ¥æ¯ã«æŽæ°ãèµ°ãããã«ããŸããã
# .github/workflows/update-rss.yml
name: update rss json file
on:
push:
branches:
- main # Set a branch name to trigger deployment
schedule:
- cron: "0 */12 * * *" # ä»åã¯åæ¥ã« 1åã®ã¿ã€ãã³ã°ã§æŽæ°ããããã«ãã
jobs:
build:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
with:
ref: main
submodules: true # Fetch Hugo themes (true OR recursive)
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Use Node.js 14.10.1
uses: actions/setup-node@v1
with:
node-version: 14.10.1
- name: Install dependencies
run: npm install
- name: Update RSS Feeds
run: npm run update-rss
- name: Commit files
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add static/rss.json
STATUS=$(git status -s)
if [ -n "$STATUS" ]; then
git commit -m "Update rss.json `date +'%Y-%m-%d %H:%M:%S'`" -a
git push origin main
fi
äžèšã¯ãŒã¯ãããŒãã¡ã€ã«ããããžã§ã¯ãã«è¿œå ããŠããªã¢ãŒããªããžããªã«ããã·ã¥ããåŸã¯ãã¯ãŒã¯ãããŒãå®è¡ãããã¿ã€ãã³ã°ãåŸ ã¡ãŸãã
ç¡äºã«ã¯ãŒã¯ãããŒã®å®è¡ãå®äºãããšäžèšã®ãããªã³ããããè¿œå ãããŠããã¯ãã§ãã
GitHub Actions ã JSON ãã¡ã€ã«ãæŽæ°ããŠã³ãããããŠãã
ã³ãããã®è©³çŽ°ãèŠããšæ£åžžã« JSON ãã¡ã€ã«ãæŽæ°ãããŠããããšã確èªã§ãã
ã³ãããåŸ Hugo ããã«ã & ãããã€ãããšããŒãžãæŽæ°ãããŠããããšã確èªã§ãã
ãã㧠Zenn ã Qiita çã«èšäºãæžããéã«ãéœåºŠæå㧠static/rss.json
ãæŽæ°ããŠããŒãžã«ææ°ã®å
容ãåæ ãããäœæ¥ã¯å¿
èŠãªããªããŸããã
(äœè«) RSS ãªãŒããŒã Hugo ã® Data Templates ã§å®è£ ãã
ã¡ãªã¿ã« Hugo ã«ã¯ Data Templates ãšããä»çµã¿ãããããããçšããããšã§å®ã¯ JavaScript ãå©çšããªããŠã HTML ãã³ãã¬ãŒã㧠RSS ãªãŒããŒãå®çŸã§ãããšããããšãåŸããç¥ããŸããã
ããã§æåŸã« Data Template ã§ã® RSS ãªãŒããŒã®å®è£ æ¹æ³ã«ã€ããŠèšèŒããŸãã
ãŸãã¯ãscripts/update-rss.mjs
ã®å
容ãæžãæããŸãã
// scripts/update-rss.mjs
import { writeFileSync } from "fs";
import pkg from "html-to-text";
const { htmlToText } = pkg;
import Parser from "rss-parser";
const parser = new Parser();
const rssFeed = {
Zenn: {
rss_url: "https://zenn.dev/nikaera/feed",
profile_url: "https://zenn.dev/nikaera",
},
Qiita: {
rss_url: "https://qiita.com/nikaera/feed.atom",
profile_url: "https://qiita.com/nikaera",
},
};
try {
const jsonFeed = {};
const spliceContent = (content) => `${htmlToText(content).slice(0, 73)}...`;
for (const [site, info] of Object.entries(rssFeed)) {
const feed = await parser.parseURL(info.rss_url);
const items = feed.items.map((i) => {
console.log(i);
return {
title: i.title,
content: spliceContent(i.content),
url: i.link,
date: i.pubDate,
};
});
const { rss_url, profile_url } = info;
jsonFeed[site] = { rss_url, profile_url, items };
/*
æçµç㪠JSON ãã¡ã€ã«ã®åºåå
㯠data ãã©ã«ããšãªããRSS ãã£ãŒãæ¯ã«åºåãã
äŸ: ./data/Qiita.json, ./data/Zenn.json, etc.
*/
writeFileSync(`./data/${site}.json`, JSON.stringify(jsonFeed[site]));
}
} catch (err) {
console.error(err);
}
äžèšãå®è¡ããããšã§ data/Qiita.json
ã data/Zenn.json
ã«ãã¡ã€ã«ãåºåãããŸãã
Hugo ã® Data Template ãçšãããš data
ãã©ã«ãå
ã«é
眮ãã json
, yaml
, toml
圢åŒã®ãã¡ã€ã«ã¯ Go ã® HTML ãã³ãã¬ãŒãã§èªã¿èŸŒããããã«ãªããŸãã
äŸãã°ãdata/Qiita.json
ã«é
眮ããã JSON ãã¡ã€ã«ãèªã¿èŸŒã¿ããå Žå㯠Go ã®ãã³ãã¬ãŒã㧠$Qiita := $.Site.Data.Qiita
ã®ãããªèšè¿°ã§ã§ããŸãã
次㫠RSS ãªãŒããŒãåã蟌ãã§ããããŒãžãäžèšã®ããã«æžãæããŸãã
<!-- ... -->
<!-- React é¢é£ã®èšè¿°ãå
šãŠåé€ãã -->
<!--
{{ with resources.Get "js/App.tsx" }}
<div id="react"></div>
{{ $options := dict "targetPath" "js/app.js" "minify" true "defines" (dict "process.env.NODE_ENV" "\"development\"") }}
{{ $js := resources.Get . | js.Build $options }}
{{ $secureJS := $js | resources.Fingerprint "sha512" }}
<script src="{{ $secureJS.Permalink }}" integrity="{{ $secureJS.Data.Integrity }}"></script>
{{ end }}
-->
<div class="archive-year">
<h2 class="archive-year-header">Tech ðŠŸ</h2>
<div class="archive-month">
<!-- data/Zenn.json ã®å
容ãèªã¿èŸŒã -->
{{ $Zenn := $.Site.Data.Zenn }}
<h3 class="archive-month-header">
<a
href="{{ $Zenn.profile_url }}"
target="_blank"
rel="noopener noreferrer"
>Zenn</a
>
-
<a href="{{ $Zenn.rss_url }}" target="_blank" rel="noopener noreferrer"
>RSS</a
>
</h3>
<div class="archive-posts">
<!-- é
åã§æ ŒçŽãããŠããèšäºæ
å ±ãç¹°ãè¿ãåŠçã§ååŸãã -->
{{- range $Zenn.items }}
<div class="archive-entry" key="{{ .url }}">
<h3 class="archive-entry-title">{{ .title }}</h3>
<div class="archive-meta">{{ .date }} - {{ .content }}</div>
<a
class="entry-link"
aria-label="{{ .content }}"
href="{{ .url }}"
target=" _blank"
rel="noopener noreferrer"
></a>
</div>
{{- end }}
</div>
</div>
<div class="archive-month">
<!-- data/Qiita.json ã®å
容ãèªã¿èŸŒã -->
{{ $Qiita := $.Site.Data.Qiita }}
<h3 class="archive-month-header">
<a
href="{{ $Qiita.profile_url }}"
target="_blank"
rel="noopener noreferrer"
>Qiita</a
>
-
<a href="{{ $Qiita.rss_url }}" target="_blank" rel="noopener noreferrer"
>RSS</a
>
</h3>
<div class="archive-posts">
<!-- é
åã§æ ŒçŽãããŠããèšäºæ
å ±ãç¹°ãè¿ãåŠçã§ååŸãã -->
{{- range $Qiita.items }}
<div class="archive-entry" key="{{ .url }}">
<h3 class="archive-entry-title">{{ .title }}</h3>
<div class="archive-meta">{{ .date }} - {{ .content }}</div>
<a
class="entry-link"
aria-label="{{ .content }}"
href="{{ .url }}"
target=" _blank"
rel="noopener noreferrer"
></a>
</div>
{{- end }}
</div>
</div>
</div>
<!-- ... -->
ãŸã GitHub Actions ã®ã¯ãŒã¯ãããŒãçšã㊠RSS ãã£ãŒãã®æ
å ±ãæŽæ°ããŠããå Žåã¯ã.github/workflows/update-rss.yml
ãã¡ã€ã«ã®æŽæ°ãå¿
èŠã«ãªããŸãã
# .github/workflows/update-rss.yml
name: update rss json file
on:
push:
branches:
- main # Set a branch name to trigger deployment
schedule:
- cron: "0 */12 * * *"
jobs:
build:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
with:
ref: main
submodules: true # Fetch Hugo themes (true OR recursive)
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Use Node.js 14.10.1
uses: actions/setup-node@v1
with:
node-version: 14.10.1
- name: Install dependencies
run: npm install
- name: Update RSS Feeds
run: npm run update-rss
# Git ã§è¿œå ããå
容ã data ãã©ã«ãã«å€æŽãã
# git add static/rss.json -> git add data/
- name: Commit files
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add data/
STATUS=$(git status -s)
if [ -n "$STATUS" ]; then
git commit -m "Update data folder `date +'%Y-%m-%d %H:%M:%S'`" -a
git push origin main
fi
ãã㧠JavaScript ã§äœæãã RSS ãªãŒããŒãããHugo ã® Data Templates ãçšããŠäœæãã RSS ãªãŒããŒãžç§»è¡ã§ããŸããã
ãããã«
Hugo 㧠React + TypeScript éçºã楜ã«ã§ããããªããšãåããããã³ã·ã§ã³ãäžãã£ãŠããŸãããã®ãŸãŸã®ããªã§å®éã« RSS ãªãŒããŒãèªããã°åãã«äœæããŠã¿ãŸããã
ããããæ¬èšäºå 容㧠RSS ãªãŒããŒãå®è£ ããã®ã§ããã°ãHugo ã® Data Templates ãå©çšããããšããã¹ããªããšã«åŸããæ°ã¥ããŸããããã Hugo ã§ã® JavaScript ãçšããéçºææ³ãç解ã§ãå匷ã«ãªã£ãã®ã§çµæãšã·ãšããŸããã
Hugo ã§ã® JavaScript éçºç°å¢ã¯çžåœå å®ããŠããããšãåãã£ãã®ã§ããŸãäœãã¢ã€ãã¢ãæãã€ãããæ°è»œã«äœã£ãŠèªããã°ã«åã蟌ãã§ãããŸããä»ã¯ã¶ãã¯ãª WebGL/WebVR ãšãã§äœãé¢çœããã®äœãããã ãªãšèããŠããŸãã
åèãªã³ã¯
- esbuild - An extremely fast JavaScript bundler
- Data Templates | Hugo
- Functions Quick Reference | Hugo
- JavaScript Building | Hugo
- Introducing Hooks â React
- rbren/rss-parser: A lightweight RSS parser, for Node and the browser
- html-to-text/node-html-to-text: Advanced html to text converter
ããèªèã«èª€ããããã°ã³ã¡ã³ãæ¬çã§ãææããã ããŸããšå¹žãã§ãã ↩︎