Repository: ebisucom/next-react-website
Branch: main
Commit: 7217e8a8ad9d
Files: 164
Total size: 103.2 KB
Directory structure:
gitextract_acizzom2/
├── README.md
├── blog/
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── README.md
│ ├── components/
│ │ ├── accordion.js
│ │ ├── contact.js
│ │ ├── container.js
│ │ ├── convert-body.js
│ │ ├── convert-date.js
│ │ ├── footer.js
│ │ ├── header.js
│ │ ├── hero.js
│ │ ├── layout.js
│ │ ├── logo.js
│ │ ├── meta.js
│ │ ├── nav.js
│ │ ├── pagination.js
│ │ ├── post-body.js
│ │ ├── post-categories.js
│ │ ├── post-header.js
│ │ ├── posts.js
│ │ ├── social.js
│ │ └── two-column.js
│ ├── jsconfig.json
│ ├── lib/
│ │ ├── api.js
│ │ ├── constants.js
│ │ ├── extract-text.js
│ │ ├── gtag.js
│ │ └── prev-next-post.js
│ ├── next.config.js
│ ├── package.json
│ ├── pages/
│ │ ├── _app.js
│ │ ├── _document.js
│ │ ├── about.js
│ │ ├── api/
│ │ │ └── hello.js
│ │ ├── blog/
│ │ │ ├── [slug].js
│ │ │ ├── category/
│ │ │ │ └── [slug].js
│ │ │ └── index.js
│ │ └── index.js
│ └── styles/
│ ├── accordion.module.css
│ ├── contact.module.css
│ ├── container.module.css
│ ├── footer.module.css
│ ├── globals.css
│ ├── header.module.css
│ ├── hero.module.css
│ ├── layout.module.css
│ ├── logo.module.css
│ ├── nav.module.css
│ ├── pagination.module.css
│ ├── post-body.module.css
│ ├── post-categories.module.css
│ ├── post-header.module.css
│ ├── posts.module.css
│ ├── social.module.css
│ ├── two-column.module.css
│ └── utils.module.css
├── blog-app-router/
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── README.md
│ ├── app/
│ │ ├── about/
│ │ │ └── page.js
│ │ ├── api/
│ │ │ ├── hello/
│ │ │ │ └── route.js
│ │ │ └── revalidate/
│ │ │ └── route.js
│ │ ├── blog/
│ │ │ ├── [slug]/
│ │ │ │ └── page.js
│ │ │ ├── category/
│ │ │ │ └── [slug]/
│ │ │ │ └── page.js
│ │ │ └── page.js
│ │ ├── layout.js
│ │ ├── not-found.js
│ │ ├── page.js
│ │ └── sitemap.js
│ ├── components/
│ │ ├── accordion.js
│ │ ├── contact.js
│ │ ├── container.js
│ │ ├── convert-body.js
│ │ ├── convert-date.js
│ │ ├── footer.js
│ │ ├── googleanalytics.js
│ │ ├── header.js
│ │ ├── hero.js
│ │ ├── layout.js
│ │ ├── logo.js
│ │ ├── meta.js
│ │ ├── nav.js
│ │ ├── pagination.js
│ │ ├── post-body.js
│ │ ├── post-categories.js
│ │ ├── post-header.js
│ │ ├── posts.js
│ │ ├── social.js
│ │ └── two-column.js
│ ├── jsconfig.json
│ ├── lib/
│ │ ├── api.js
│ │ ├── baseMetadata.js
│ │ ├── constants.js
│ │ ├── extract-text.js
│ │ ├── gtag.js
│ │ └── prev-next-post.js
│ ├── next.config.js
│ ├── package.json
│ └── styles/
│ ├── accordion.module.css
│ ├── contact.module.css
│ ├── container.module.css
│ ├── footer.module.css
│ ├── globals.css
│ ├── header.module.css
│ ├── hero.module.css
│ ├── layout.module.css
│ ├── logo.module.css
│ ├── nav.module.css
│ ├── pagination.module.css
│ ├── post-body.module.css
│ ├── post-categories.module.css
│ ├── post-header.module.css
│ ├── posts.module.css
│ ├── social.module.css
│ ├── two-column.module.css
│ └── utils.module.css
├── blog-chap6/
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── README.md
│ ├── components/
│ │ ├── contact.js
│ │ ├── container.js
│ │ ├── footer.js
│ │ ├── header.js
│ │ ├── hero.js
│ │ ├── layout.js
│ │ ├── logo.js
│ │ ├── meta.js
│ │ ├── nav.js
│ │ ├── post-body.js
│ │ ├── social.js
│ │ └── two-column.js
│ ├── jsconfig.json
│ ├── lib/
│ │ └── constants.js
│ ├── next.config.js
│ ├── package.json
│ ├── pages/
│ │ ├── _app.js
│ │ ├── _document.js
│ │ ├── about.js
│ │ ├── api/
│ │ │ └── hello.js
│ │ ├── blog/
│ │ │ └── index.js
│ │ └── index.js
│ └── styles/
│ ├── contact.module.css
│ ├── container.module.css
│ ├── footer.module.css
│ ├── globals.css
│ ├── header.module.css
│ ├── hero.module.css
│ ├── layout.module.css
│ ├── logo.module.css
│ ├── nav.module.css
│ ├── post-body.module.css
│ ├── social.module.css
│ ├── two-column.module.css
│ └── utils.module.css
├── figma/
│ └── next-react-website.fig
├── images-local/
│ ├── README.md
│ └── README.txt
├── images-post/
│ ├── README.md
│ └── README.txt
└── import/
├── api-blogs.json
├── api-categories.json
├── contents-blogs.csv
└── contents-categories.csv
================================================
FILE CONTENTS
================================================
================================================
FILE: README.md
================================================
# 作って学ぶ Next.js/React Webサイト構築

---
**plaiceholderのバージョンについて[サポートWiki](https://github.com/ebisucom/next-react-website/wiki)に追加しました。 [2023年8月13日]**
**「Next.js 13 対応ガイド」(next13.pdf)を速報版から更新しました。 [2023年7月5日]**
* **「Next.js 13で本書を進める方法」**
* **「本書のプロジェクトをApp Routerへ移行する方法」**
**をまとめています。**
※最新の情報は著者NOTE(https://ebisu.com/note/ ) や Twitter ( https://twitter.com/ebisucom ) でも出していますので、参考にしてください。
---
**ダウンロードデータ**
セットアップPDFや、本書で作成するプロジェクトデータ、使用する画像素材、インポート用のコンテンツデータなどを収録しています。詳しくは、本書を参照してください。
* [サポートサイト](https://book.mynavi.jp/supportsite/detail/9784839980177.html)
* [書籍情報](https://ebisu.com/next-react-website/)
## ■【副読本:Next.js 13 対応ガイド】next13.pdf
* 「Next.js 13で本書を進める方法」
* 「本書のプロジェクトをApp Routerへ移行する方法」
をまとめたPDFです。
## ■【副読本:セットアップPDF】setup.pdf
開発環境などを準備する方法についてまとめたPDFです。
### setup.pdfの内容
* アカウントの作成[GitHub、microCMS、Vercel、Netlify、Figma]
* 開発環境の準備[Node.js、Git、エディタ(Visual Studio Code)]
* サイトの公開と更新
* microCMSで管理するコンテンツの準備
* Figmaとデザインデータの使い方
## ■【プロジェクトデータ】
本書で作成するブログサイトのプロジェクトデータです。
ディレクトリ | 内容
---------------- | -----
blog/ | Chapter 10で完成させるプロジェクトデータです。microCMSのデータを使用します。
blog-chap6/ | Chapter 6のプロジェクトデータです。外部データは使用しません。
blog-app-router/ | 「Next.js 13 対応ガイド」(next13.pdf)の「本書のプロジェクトをApp Routerへ移行する方法」で完成させるプロジェクトデータです。
※プロジェクトデータをそのまま利用する場合、各ディレクトリのルートで
```
npm install
```
を実行してください。
## ■【デモ】
本書で作成するブログサイトをVercelとNetlifyにデプロイしたものです。
* https://next-react-website.vercel.app
* https://next-react-website.netlify.app
## ■【インポートデータ】
microCMS用のフィールドとコンテンツのインポートデータです。
ディレクトリ | ファイル | 内容
------------ | ----------------------- | ---------------------
import/ | api-blogs.json | ブログAPIのフィールド
| contents-blogs.csv | ブログ記事のコンテンツ
| api-categories.json | カテゴリAPIのフィールド
| contents-categories.csv | カテゴリのコンテンツ
## ■【画像データ】
ブログサイトで使用する画像データです。
ディレクトリ | 内容
------------- | -----
images-local/ | ローカル環境で使用する画像です。
images-post/ | microCMSで使用するアイキャッチ画像と記事本文に挿入する画像です。
## ■【Figmaのデザインデータ】
Figmaで作成したデザインデータです。
ディレクトリ | ファイル&ディレクトリ | 内容
------------ | ----------------------- | ---------------------------------------------
Figma/ | next-react-website.fig | Figmaのデザインデータ
| jpg/ | FigmaのデザインデータをJPEG画像として出力したもの
## ■ ブログサイトのコンテンツについて
ブログサイトのコンテンツは、日本語に特化したrinna株式会社のGPT言語モデル(rinna/japanese-gpt-1b)を使用して生成した文章を編集したものです。
* [rinna/japanese-gpt-1b](https://huggingface.co/rinna/japanese-gpt-1b) [[License](https://huggingface.co/rinna/japanese-gpt-1b#licenese)]
-----------------------------------------------
## ■ ご利用にあたって
本書に記載されている内容や本ダウンロードデータの運用によって、いかなる損害が生じても、株式会社マイナビ出版および著者は責任を負いかねますので、あらかじめご了承ください。
(c)2022 EBISUCOM / マイナビ出版
================================================
FILE: blog/.eslintrc.json
================================================
{
"extends": "next/core-web-vitals"
}
================================================
FILE: blog/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
================================================
FILE: blog/README.md
================================================
# ブログサイトのプロジェクト
『作って学ぶ Next.js/React Webサイト構築』のChapter 10で完成させるプロジェクトデータです。
* [サポートサイト](https://book.mynavi.jp/supportsite/detail/9784839980177.html)
* [書籍情報](https://ebisu.com/next-react-website/)
================================================
FILE: blog/components/accordion.js
================================================
import { useState, useRef } from 'react'
import styles from 'styles/accordion.module.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCircleChevronDown } from '@fortawesome/free-solid-svg-icons'
export default function Accordion({ heading, children }) {
const [textIsOpen, setTextIsOpen] = useState(false)
const toggleText = () => {
setTextIsOpen((prev) => !prev)
}
const refText = useRef(null)
return (
)
}
================================================
FILE: blog/components/contact.js
================================================
import Social from 'components/social'
import styles from 'styles/contact.module.css'
export default function Contact() {
return (
Contact
cube@web.mail.address
)
}
================================================
FILE: blog/components/container.js
================================================
import styles from 'styles/container.module.css'
export default function Container({ children, large = false }) {
return (
{children}
)
}
================================================
FILE: blog/components/convert-body.js
================================================
import parse from 'html-react-parser'
import Image from 'next/image'
export default function ConvertBody({ contentHTML }) {
const contentReact = parse(contentHTML, {
replace: (node) => {
if (node.name === 'img') {
const { src, alt, width, height } = node.attribs
return (
)
}
},
})
return <>{contentReact}>
}
================================================
FILE: blog/components/convert-date.js
================================================
import { parseISO, format } from 'date-fns'
import ja from 'date-fns/locale/ja'
export default function ConvertDate({ dateISO }) {
return (
{format(parseISO(dateISO), 'yyyy年MM月dd日', {
locale: ja,
})}
)
}
================================================
FILE: blog/components/footer.js
================================================
import Container from 'components/container'
import Logo from 'components/logo'
import Social from 'components/social'
import styles from 'styles/footer.module.css'
export default function Footer() {
return (
)
}
================================================
FILE: blog/components/header.js
================================================
import Container from 'components/container'
import Logo from 'components/logo'
import Nav from 'components/nav'
import styles from 'styles/header.module.css'
export default function Header() {
return (
)
}
================================================
FILE: blog/components/hero.js
================================================
import styles from 'styles/hero.module.css'
import Image from 'next/image'
import cube from 'images/cube.jpg'
export default function Hero({ title, subtitle, imageOn = false }) {
return (
)
}
================================================
FILE: blog/components/layout.js
================================================
import Header from 'components/header'
import Footer from 'components/footer'
export default function Layout({ children }) {
return (
<>
{children}
>
)
}
================================================
FILE: blog/components/logo.js
================================================
import Link from 'next/link'
import styles from 'styles/logo.module.css'
export default function Logo({ boxOn = false }) {
return (
CUBE
)
}
================================================
FILE: blog/components/meta.js
================================================
import Head from 'next/head'
import { useRouter } from 'next/router'
// サイトに関する情報
import { siteMeta } from 'lib/constants'
const { siteTitle, siteDesc, siteUrl, siteLocale, siteType, siteIcon } =
siteMeta
// 汎用OGP画像
import siteImg from 'images/ogp.jpg'
export default function Meta({
pageTitle,
pageDesc,
pageImg,
pageImgW,
pageImgH,
}) {
// ページのタイトル
const title = pageTitle ? `${pageTitle} | ${siteTitle}` : siteTitle
// ページの説明
const desc = pageDesc ?? siteDesc
// ページのURL
const router = useRouter()
const url = `${siteUrl}${router.asPath}`
// OGP画像
const img = pageImg || siteImg.src
const imgW = pageImgW || siteImg.width
const imgH = pageImgH || siteImg.height
const imgUrl = img.startsWith('https') ? img : `${siteUrl}${img}`
return (
{title}
)
}
================================================
FILE: blog/components/nav.js
================================================
import { useState } from 'react'
import Link from 'next/link'
import styles from 'styles/nav.module.css'
export default function Nav() {
const [navIsOpen, setNavIsOpen] = useState(false)
const toggleNav = () => {
setNavIsOpen((prev) => !prev)
}
const closeNav = () => {
setNavIsOpen(false)
}
return (
{navIsOpen && (
)}
MENU
)
}
================================================
FILE: blog/components/pagination.js
================================================
import styles from 'styles/pagination.module.css'
import Link from 'next/link'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faChevronLeft,
faChevronRight,
} from '@fortawesome/free-solid-svg-icons'
export default function Pagination({
prevText = '',
prevUrl = '',
nextText = '',
nextUrl = '',
}) {
return (
)
}
================================================
FILE: blog/components/post-body.js
================================================
import styles from 'styles/post-body.module.css'
export default function PostBody({ children }) {
return (
{children}
)
}
================================================
FILE: blog/components/post-categories.js
================================================
import styles from 'styles/post-categories.module.css'
import Link from 'next/link'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFolderOpen } from '@fortawesome/free-regular-svg-icons'
export default function PostCategories({ categories }) {
return (
Categories
{categories.map(({ name, slug }) => (
{name}
))}
)
}
================================================
FILE: blog/components/post-header.js
================================================
import styles from 'styles/post-header.module.css'
import ConvertDate from 'components/convert-date'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faClock } from '@fortawesome/free-regular-svg-icons'
export default function PostHeader({ title, subtitle, publish = '' }) {
return (
{subtitle}
{title}
{publish && (
)}
)
}
================================================
FILE: blog/components/posts.js
================================================
import styles from 'styles/posts.module.css'
import Link from 'next/link'
import Image from 'next/image'
export default function Posts({ posts }) {
return (
{posts.map(({ title, slug, eyecatch }) => (
{title}
))}
)
}
================================================
FILE: blog/components/social.js
================================================
import styles from 'styles/social.module.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faTwitter,
faFacebookF,
faGithub,
} from '@fortawesome/free-brands-svg-icons'
export default function Social({ iconSize = 'initial' }) {
return (
)
}
================================================
FILE: blog/components/two-column.js
================================================
import styles from 'styles/two-column.module.css'
export function TwoColumn({ children }) {
return {children}
}
export function TwoColumnMain({ children }) {
return {children}
}
export function TwoColumnSidebar({ children }) {
return {children}
}
================================================
FILE: blog/jsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": "."
}
}
================================================
FILE: blog/lib/api.js
================================================
import { createClient } from 'microcms-js-sdk'
export const client = createClient({
serviceDomain: process.env.SERVICE_DOMAIN,
apiKey: process.env.API_KEY,
})
export async function getPostBySlug(slug) {
try {
const post = await client.get({
endpoint: 'blogs',
queries: { filters: `slug[equals]${slug}` },
})
return post.contents[0]
} catch (err) {
console.log('~~ getPostBySlug ~~')
console.log(err)
}
}
export async function getAllSlugs(limit = 100) {
try {
const slugs = await client.get({
endpoint: 'blogs',
queries: { fields: 'title,slug', orders: '-publishDate', limit: limit },
})
return slugs.contents
} catch (err) {
console.log('~~ getAllSlugs ~~')
console.log(err)
}
}
export async function getAllPosts(limit = 100) {
try {
const posts = await client.get({
endpoint: 'blogs',
queries: {
fields: 'title,slug,eyecatch',
orders: '-publishDate',
limit: limit,
},
})
return posts.contents
} catch (err) {
console.log('~~ getAllPosts ~~')
console.log(err)
}
}
export async function getAllCategories(limit = 100) {
try {
const categories = await client.get({
endpoint: 'categories',
queries: {
fields: 'name,id,slug',
limit: limit,
},
})
return categories.contents
} catch (err) {
console.log('~~ getAllCategories ~~')
console.log(err)
}
}
export async function getAllPostsByCategory(catID, limit = 100) {
try {
const posts = await client.get({
endpoint: 'blogs',
queries: {
filters: `categories[contains]${catID}`,
fields: 'title,slug,eyecatch',
orders: '-publishDate',
limit: limit,
},
})
return posts.contents
} catch (err) {
console.log('~~ getAllPostsByCategory ~~')
console.log(err)
}
}
================================================
FILE: blog/lib/constants.js
================================================
export const siteMeta = {
siteTitle: 'CUBE',
siteDesc: 'アウトプットしていくサイト',
siteUrl: 'https://*********',
siteLang: 'ja',
siteLocale: 'ja_JP',
siteType: 'website',
siteIcon: '/favicon.png',
}
export const eyecatchLocal = {
url: '/eyecatch.jpg',
width: 1920,
height: 1280,
}
================================================
FILE: blog/lib/extract-text.js
================================================
import { convert } from 'html-to-text'
export function extractText(html, length = 80, more = '…') {
const text = convert(html, {
selectors: [
{ selector: 'img', format: 'skip' },
{ selector: 'a', options: { ignoreHref: true } },
],
})
return text.slice(0, length) + more
}
================================================
FILE: blog/lib/gtag.js
================================================
export const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_ID
export const pageview = (url) => {
window.gtag('config', GA_MEASUREMENT_ID, {
page_path: url,
})
}
================================================
FILE: blog/lib/prev-next-post.js
================================================
export function prevNextPost(allSlugs, currentSlug) {
const numberOfPosts = allSlugs.length
const index = allSlugs.findIndex(({ slug }) => slug === currentSlug)
const prevPost =
index + 1 === numberOfPosts ? { title: '', slug: '' } : allSlugs[index + 1]
const nextPost = index === 0 ? { title: '', slug: '' } : allSlugs[index - 1]
return [prevPost, nextPost]
}
================================================
FILE: blog/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
domains: ['images.microcms-assets.io'],
},
}
module.exports = nextConfig
================================================
FILE: blog/package.json
================================================
{
"name": "blog",
"private": true,
"description": "「作って学ぶ Next.js/React Webサイト構築」のChapter 10で完成させるプロジェクトデータです",
"homepage": "https://github.com/ebisucom/next-react-website",
"author": "エビスコム - EBISUCOM (https://ebisu.com/)",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-brands-svg-icons": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.1.18",
"date-fns": "^2.28.0",
"html-react-parser": "^2.0.0",
"html-to-text": "^8.2.0",
"microcms-js-sdk": "^2.0.0",
"next": "12.2.0",
"plaiceholder": "^2.4.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"sharp": "^0.30.7"
},
"devDependencies": {
"eslint": "^8.29.0",
"eslint-config-next": "^13.0.6"
}
}
================================================
FILE: blog/pages/_app.js
================================================
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import 'styles/globals.css'
import Layout from 'components/layout'
import Script from 'next/script'
import * as gtag from 'lib/gtag'
// Font Awesomeの設定
import '@fortawesome/fontawesome-svg-core/styles.css'
import { config } from '@fortawesome/fontawesome-svg-core'
config.autoAddCss = false
function MyApp({ Component, pageProps }) {
const router = useRouter()
useEffect(() => {
const handleRouteChange = (url) => {
gtag.pageview(url)
}
router.events.on('routeChangeComplete', handleRouteChange)
return () => {
router.events.off('routeChangeComplete', handleRouteChange)
}
}, [router.events])
return (
<>
>
)
}
export default MyApp
================================================
FILE: blog/pages/_document.js
================================================
import { Html, Head, Main, NextScript } from 'next/document'
import { siteMeta } from 'lib/constants'
const { siteLang } = siteMeta
export default function Document() {
return (
)
}
================================================
FILE: blog/pages/about.js
================================================
import Meta from 'components/meta'
import Container from 'components/container'
import Hero from 'components/hero'
import PostBody from 'components/post-body'
import Contact from 'components/contact'
import {
TwoColumn,
TwoColumnMain,
TwoColumnSidebar,
} from 'components/two-column'
import Accordion from 'components/accordion'
import Image from 'next/image'
import eyecatch from 'images/about.jpg'
export default function About() {
return (
Cubeが得意とする分野はモノづくりです。3次元から2次元の造形、プログラミングやデザインなど、さまざまな技術を組み合わせることによって社会や環境と結びつけるクリエイティブを提案し続けています。
モノづくりで目指していること
モノづくりではデータの解析からクリエイティブまで幅広いことを担当しています。新しいことを取り入れながら、ユーザーにマッチした提案を実現するのが目標です。たくさんの開発・提供が数多くありますが、特にそこを磨く作業に力を入れています。
単純に形にするだけでなく、作る過程や、なぜそのようにしたのかを大事にしながらものづくりをしています。毎回課題解決テーマをもって「モノ」と向き合い制作をし、フィードバックしてもらうことで自分の中にあるモヤモヤを言葉にして「問い」への答えを出しています。
新しいことへのチャレンジ
今までと違うものを作ることで愛着が湧いてきます。そこで興味を持ったことは小さなことでもいいから取り入れて、良いものを作れるようにしています。小さなヒントから新しいものを生み出すようなモノづくりは、これからも続けていきたいです。
FAQ
プログラミングのポイントは、作りたいものを作ることです。楽しいことから思いつき、目標とゴールを決め、そこに向かってさまざまな課題を設定していきながら、プログラムを作っていきます。
古代語を解読するのに必要なのは、書かれた文字そのものだけです。古代の世界観や思考方法。それらを読み取ってこそ古代の世界観が理解できてきます。
公開リポジトリを活用すると、全世界のどこからでもアクセスし、開発者が関連するプロジェクトのタスクを利用することができます。
)
}
================================================
FILE: blog/pages/api/hello.js
================================================
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' })
}
================================================
FILE: blog/pages/blog/[slug].js
================================================
import { getPostBySlug, getAllSlugs } from 'lib/api'
import { extractText } from 'lib/extract-text'
import { prevNextPost } from 'lib/prev-next-post'
import Meta from 'components/meta'
import Container from 'components/container'
import PostHeader from 'components/post-header'
import PostBody from 'components/post-body'
import {
TwoColumn,
TwoColumnMain,
TwoColumnSidebar,
} from 'components/two-column'
import ConvertBody from 'components/convert-body'
import PostCategories from 'components/post-categories'
import Pagination from 'components/pagination'
import Image from 'next/image'
import { getPlaiceholder } from 'plaiceholder'
// ローカルの代替アイキャッチ画像
import { eyecatchLocal } from 'lib/constants'
export default function Post({
title,
publish,
content,
eyecatch,
categories,
description,
prevPost,
nextPost,
}) {
return (
)
}
export async function getStaticPaths() {
const allSlugs = await getAllSlugs()
return {
paths: allSlugs.map(({ slug }) => `/blog/${slug}`),
fallback: false,
}
}
export async function getStaticProps(context) {
const slug = context.params.slug
const post = await getPostBySlug(slug)
const description = extractText(post.content)
const eyecatch = post.eyecatch ?? eyecatchLocal
const { base64 } = await getPlaiceholder(eyecatch.url)
eyecatch.blurDataURL = base64
const allSlugs = await getAllSlugs()
const [prevPost, nextPost] = prevNextPost(allSlugs, slug)
return {
props: {
title: post.title,
publish: post.publishDate,
content: post.content,
eyecatch: eyecatch,
categories: post.categories,
description: description,
prevPost: prevPost,
nextPost: nextPost,
},
}
}
================================================
FILE: blog/pages/blog/category/[slug].js
================================================
import { getAllCategories, getAllPostsByCategory } from 'lib/api'
import Meta from 'components/meta'
import Container from 'components/container'
import PostHeader from 'components/post-header'
import Posts from 'components/posts'
import { getPlaiceholder } from 'plaiceholder'
// ローカルの代替アイキャッチ画像
import { eyecatchLocal } from 'lib/constants'
export default function Category({ name, posts }) {
return (
)
}
export async function getStaticPaths() {
const allCats = await getAllCategories()
return {
paths: allCats.map(({ slug }) => `/blog/category/${slug}`),
fallback: false,
}
}
export async function getStaticProps(context) {
const catSlug = context.params.slug
const allCats = await getAllCategories()
const cat = allCats.find(({ slug }) => slug === catSlug)
const posts = await getAllPostsByCategory(cat.id)
for (const post of posts) {
if (!post.hasOwnProperty('eyecatch')) {
post.eyecatch = eyecatchLocal
}
const { base64 } = await getPlaiceholder(post.eyecatch.url)
post.eyecatch.blurDataURL = base64
}
return {
props: {
name: cat.name,
posts: posts,
},
}
}
================================================
FILE: blog/pages/blog/index.js
================================================
import { getAllPosts } from 'lib/api'
import Meta from 'components/meta'
import Container from 'components/container'
import Hero from 'components/hero'
import Posts from 'components/posts'
import { getPlaiceholder } from 'plaiceholder'
// ローカルの代替アイキャッチ画像
import { eyecatchLocal } from 'lib/constants'
export default function Blog({ posts }) {
return (
)
}
export async function getStaticProps() {
const posts = await getAllPosts()
for (const post of posts) {
if (!post.hasOwnProperty('eyecatch')) {
post.eyecatch = eyecatchLocal
}
const { base64 } = await getPlaiceholder(post.eyecatch.url)
post.eyecatch.blurDataURL = base64
}
return {
props: {
posts: posts,
},
}
}
================================================
FILE: blog/pages/index.js
================================================
import { getAllPosts } from 'lib/api'
import Meta from 'components/meta'
import Container from 'components/container'
import Hero from 'components/hero'
import Posts from 'components/posts'
import Pagination from 'components/pagination'
import { getPlaiceholder } from 'plaiceholder'
// ローカルの代替アイキャッチ画像
import { eyecatchLocal } from 'lib/constants'
export default function Home({ posts }) {
return (
)
}
export async function getStaticProps() {
const posts = await getAllPosts(4)
for (const post of posts) {
if (!post.hasOwnProperty('eyecatch')) {
post.eyecatch = eyecatchLocal
}
const { base64 } = await getPlaiceholder(post.eyecatch.url)
post.eyecatch.blurDataURL = base64
}
return {
props: {
posts: posts,
},
}
}
================================================
FILE: blog/styles/accordion.module.css
================================================
.open,
.close {
border: solid 1px var(--gray-25);
}
/* 見出し */
.heading {
font-size: var(--body);
}
.heading button {
all: unset;
outline: revert;
-webkit-tap-highlight-color: transparent;
cursor: pointer;
box-sizing: border-box;
width: 100%;
padding: 1em;
display: flex;
justify-content: space-between;
gap: 1em;
}
/* 見出しのアイコン */
.icon {
color: var(--gray-25);
font-size: 1.25em;
transition: transform 0.5s;
}
.open .icon {
transform: rotate(180deg);
}
/* テキスト */
.text {
overflow: hidden;
height: 0px;
transition: none;
}
.open .text {
animation: openAnim 0.5s forwards;
}
.close .text {
animation: closeAnim 0.5s forwards;
}
@keyframes openAnim {
0% {
height: 0px;
}
99% {
height: var(--text-height);
}
100% {
height: auto;
}
}
@keyframes closeAnim {
0% {
height: var(--text-height);
}
100% {
height: 0px;
}
}
.textInner {
padding: 0 1.14em 1.14em;
font-size: calc(var(--body) * 0.875);
}
================================================
FILE: blog/styles/contact.module.css
================================================
.stack > * + * {
margin-top: var(--stack-space, 1em);
}
.heading {
font-size: var(--body);
}
================================================
FILE: blog/styles/container.module.css
================================================
.default {
width: 92%;
max-width: 1152px;
margin: 0 auto;
}
.large {
composes: default;
max-width: 1280px;
}
================================================
FILE: blog/styles/footer.module.css
================================================
.wrapper {
padding: var(--space-xl) 0;
background-color: var(--gray-10);
}
.flexContainer {
composes: sideBySideCenter from './utils.module.css';
gap: 2em;
}
================================================
FILE: blog/styles/globals.css
================================================
:root {
/* カラー(色) */
--white: #ffffff;
--gray-10: #eeeeee;
--gray-25: #aaaaaa;
--gray-50: #707070;
--gray-75: #444444;
--black: #222222;
--accent: #0d87e0;
/* タイポグラフィ(フォントサイズ) */
--body: clamp(1rem, 0.95rem + 0.2vw, 1.125rem); /* 16-18px */
--display: clamp(4.5rem, 1.83rem + 11.34vw, 10rem); /* 72-160 */
--heading1: clamp(2rem, 1.3rem + 3vw, 4rem); /* 32-64px */
--heading2: calc(var(--body) * 1.5); /* 24-27px */
--heading3: calc(var(--body) * 1.2); /* 19.2-21.6px */
--small-heading2: clamp(0.875rem, 4vw - 1rem, 1.6875rem); /* 14-27px */
--small-heading3: calc(var(--small-heading2) * 0.86); /* 12-23px */
/* スペース(余白・間隔) */
--space-xs: clamp(1.25rem, 1rem + 0.98vw, 1.875rem); /* 20-30px */
--space-sm: calc(var(--space-xs) * 1.5); /* 30-45px */
--space-md: calc(var(--space-xs) * 2); /* 40-60px */
--space-lg: calc(var(--space-xs) * 3); /* 60-90px */
--space-xl: calc(var(--space-xs) * 4); /* 80-120px */
--space-jump: clamp(1.25rem, 0.35rem + 3.8vw, 3.75rem); /* 20-60px */
}
/* 基本設定 */
body {
color: var(--black);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial,
sans-serif;
font-size: var(--body);
}
h1 {
font-size: var(--heading1);
}
h2 {
font-size: var(--heading2);
}
h3 {
font-size: var(--heading3);
}
/* next/image */
span > img {
transition: 0.2s;
}
/* リセット */
body,
h1,
h2,
h3,
p,
figure,
ul {
margin: 0;
padding: 0;
list-style: none;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
a {
color: inherit;
text-decoration: none;
}
================================================
FILE: blog/styles/header.module.css
================================================
.flexContainer {
composes: spaceBetween from 'styles/utils.module.css';
}
================================================
FILE: blog/styles/hero.module.css
================================================
.flexContainer {
composes: sideBySideCenter from 'styles/utils.module.css';
}
.text {
padding-top: calc(var(--display) * 0.5);
padding-bottom: calc(var(--display) * 0.7);
}
.title {
font-size: var(--display);
font-weight: 900;
letter-spacing: 0.15em;
}
.subtitle {
font-size: var(--small-heading2);
}
/* image */
.image {
width: 100%;
}
@media (min-width: 768px) {
.image {
width: 50%;
}
}
================================================
FILE: blog/styles/layout.module.css
================================================
================================================
FILE: blog/styles/logo.module.css
================================================
.basic {
font-size: var(--heading2);
font-weight: 700;
letter-spacing: 0.15em;
}
.box {
composes: basic;
display: inline-block;
padding: 1em 2em;
background-color: var(--gray-75);
color: var(--white);
font-size: var(--small-heading2);
}
================================================
FILE: blog/styles/nav.module.css
================================================
/* デスクトップ */
@media (min-width: 768px) {
.btn {
display: none;
}
.list {
display: flex;
gap: 2em;
}
}
/* モバイル */
@media (max-width: 767px) {
/* ボタン */
.btn {
all: unset;
outline: revert;
-webkit-tap-highlight-color: transparent;
cursor: pointer;
width: 42px;
height: 42px;
position: relative;
z-index: 200;
}
.close .btn {
color: var(--gray-75);
}
.open .btn {
color: var(--white);
}
/* ボタン内のバー */
.btn {
display: grid;
place-items: center;
}
.btn::before,
.btn::after,
.btn .bar {
grid-area: 1 / 1;
content: '';
display: block;
width: 32px;
height: 1px;
background-color: currentColor;
transition: transform 0.4s;
}
.close .btn::before {
transform: translateY(-8px);
}
.close .btn::after {
transform: translateY(8px);
}
.open .btn::before {
transform: rotate(45deg);
}
.open .btn::after {
transform: rotate(-45deg);
}
.open .btn .bar {
transform: scale(0);
}
/* メニュー(オーバーレイ) */
.list {
position: fixed;
inset: 0 -100% 0 100%;
z-index: 100;
background: rgba(0, 0, 0, 0.8);
color: var(--white);
transition: transform 0.4s;
}
.open .list {
transform: translateX(-100%);
}
/* メニュー(オーバーレイ内の配置) */
.list {
display: grid;
gap: 40px;
place-content: center;
text-align: center;
}
}
/* ホバー */
@media (hover: hover) {
.list a:hover {
color: var(--accent);
}
}
@media (hover: none) {
.list a {
-webkit-tap-highlight-color: transparent;
}
.list a:active {
color: var(--accent);
}
}
================================================
FILE: blog/styles/pagination.module.css
================================================
.flexContainer {
composes: spaceBetween from 'styles/utils.module.css';
margin: var(--space-lg) 0;
gap: 1em;
}
.next {
margin-left: auto;
}
.iconText {
display: flex;
align-items: center;
gap: 0.5em;
}
================================================
FILE: blog/styles/post-body.module.css
================================================
.stack > * + * {
margin-top: var(--stack-space, 1.5em);
}
.stack h2,
.stack h3 {
--stack-space: 2em;
}
.stack h2 + *,
.stack h3 + * {
--stack-space: 0.8em;
}
.stack p {
line-height: 1.8;
}
.stack ul {
padding: revert;
list-style: revert;
}
================================================
FILE: blog/styles/post-categories.module.css
================================================
.flexContainer {
display: flex;
align-items: baseline;
gap: 1.25rem;
color: var(--gray-50);
}
@media (min-width: 768px) {
.flexContainer {
flex-direction: column;
}
}
.heading {
font-size: var(--small-heading2);
}
.list {
composes: flexContainer;
font-size: var(--small-heading3);
gap: 0.75rem;
}
================================================
FILE: blog/styles/post-header.module.css
================================================
.stack {
padding: var(--space-sm) 0;
}
.stack > * + * {
margin-top: var(--stack-space, 1em);
}
.subtitle {
font-size: var(--small-heading2);
font-weight: 700;
}
.title {
--stack-space: 0.2em;
}
.publish {
display: flex;
gap: 0.5em;
color: var(--gray-50);
font-size: var(--small-heading3);
}
================================================
FILE: blog/styles/posts.module.css
================================================
.gridContainer {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-jump);
margin-top: var(--space-xs);
margin-bottom: var(--space-lg);
}
.post h2 {
margin-top: 0.5em;
font-size: var(--small-heading3);
font-weight: 400;
}
.post figure {
position: relative;
aspect-ratio: 16/9;
}
================================================
FILE: blog/styles/social.module.css
================================================
.list {
display: flex;
gap: 1.5em;
font-size: var(--icon-size, 24px);
}
================================================
FILE: blog/styles/two-column.module.css
================================================
.flexContainer {
composes: sideBySide from './utils.module.css';
gap: var(--space-md);
margin: var(--space-md) 0 var(--space-lg);
}
@media (min-width: 768px) {
.main {
width: 768px;
}
.sidebar {
width: 240px;
position: sticky;
top: 40px;
align-self: flex-start;
}
.sidebar * {
text-align: right;
}
.sidebar :is(div, ul) {
width: fit-content;
margin-left: auto;
place-items: flex-end;
place-content: flex-end;
}
}
================================================
FILE: blog/styles/utils.module.css
================================================
/* 両端揃え */
.spaceBetween {
display: flex;
justify-content: space-between;
align-items: center;
}
/* 横並び(基本形) */
.sideBySide {
display: flex;
flex-direction: column;
}
@media (min-width: 768px) {
.sideBySide {
flex-direction: row;
justify-content: space-between;
}
}
/* 横並び(中央揃え) */
.sideBySideCenter {
composes: sideBySide;
align-items: center;
text-align: center;
}
@media (min-width: 768px) {
.sideBySideCenter {
text-align: left;
}
}
================================================
FILE: blog-app-router/.eslintrc.json
================================================
{
"extends": "next/core-web-vitals"
}
================================================
FILE: blog-app-router/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
================================================
FILE: blog-app-router/README.md
================================================
# ブログサイトのプロジェクト
『作って学ぶ Next.js/React Webサイト構築』の副読本「Next.js 13 対応ガイド」の「本書のプロジェクトをApp Routerへ移行する方法」で完成させるプロジェクトデータです。
* .env.local
* next.config.js の remotePatterns
の設定が必要です。
---
* [サポートサイト](https://book.mynavi.jp/supportsite/detail/9784839980177.html)
* [書籍情報](https://ebisu.com/next-react-website/)
================================================
FILE: blog-app-router/app/about/page.js
================================================
import Container from 'components/container'
import Hero from 'components/hero'
import PostBody from 'components/post-body'
import Contact from 'components/contact'
import {
TwoColumn,
TwoColumnMain,
TwoColumnSidebar,
} from 'components/two-column'
import Accordion from 'components/accordion'
import Image from 'next/legacy/image'
import eyecatch from 'images/about.jpg'
// サイトに関する情報
import { siteMeta } from 'lib/constants'
const { siteTitle, siteUrl } = siteMeta
// ベースのメタデータ
import { openGraphMetadata, twitterMetadata } from 'lib/baseMetadata'
export default function About() {
return (
Cubeが得意とする分野はモノづくりです。3次元から2次元の造形、プログラミングやデザインなど、さまざまな技術を組み合わせることによって社会や環境と結びつけるクリエイティブを提案し続けています。
モノづくりで目指していること
モノづくりではデータの解析からクリエイティブまで幅広いことを担当しています。新しいことを取り入れながら、ユーザーにマッチした提案を実現するのが目標です。たくさんの開発・提供が数多くありますが、特にそこを磨く作業に力を入れています。
単純に形にするだけでなく、作る過程や、なぜそのようにしたのかを大事にしながらものづくりをしています。毎回課題解決テーマをもって「モノ」と向き合い制作をし、フィードバックしてもらうことで自分の中にあるモヤモヤを言葉にして「問い」への答えを出しています。
新しいことへのチャレンジ
今までと違うものを作ることで愛着が湧いてきます。そこで興味を持ったことは小さなことでもいいから取り入れて、良いものを作れるようにしています。小さなヒントから新しいものを生み出すようなモノづくりは、これからも続けていきたいです。
FAQ
プログラミングのポイントは、作りたいものを作ることです。楽しいことから思いつき、目標とゴールを決め、そこに向かってさまざまな課題を設定していきながら、プログラムを作っていきます。
古代語を解読するのに必要なのは、書かれた文字そのものだけです。古代の世界観や思考方法。それらを読み取ってこそ古代の世界観が理解できてきます。
公開リポジトリを活用すると、全世界のどこからでもアクセスし、開発者が関連するプロジェクトのタスクを利用することができます。
)
}
// メタデータ
const pageTitle = 'アバウト'
const pageDesc = 'About development activities'
const ogpTitle = `${pageTitle} | ${siteTitle}`
const ogpUrl = new URL('/about', siteUrl).toString()
export const metadata = {
title: pageTitle,
description: pageDesc,
openGraph: {
...openGraphMetadata,
title: ogpTitle,
description: pageDesc,
url: ogpUrl,
images: [
{
url: eyecatch.src,
width: eyecatch.width,
height: eyecatch.height,
},
],
},
twitter: {
...twitterMetadata,
title: ogpTitle,
description: pageDesc,
images: [eyecatch.src],
},
}
================================================
FILE: blog-app-router/app/api/hello/route.js
================================================
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({ name: 'John Doe' })
}
================================================
FILE: blog-app-router/app/api/revalidate/route.js
================================================
import { NextResponse } from 'next/server'
import { revalidatePath } from 'next/cache'
export async function GET(request) {
const { searchParams } = new URL(request.url)
const secret = searchParams.get('secret')
if (secret !== process.env.SECRET_TOKEN) {
return NextResponse.json({ message: 'Invalid token' }, { status: 401 })
}
try {
revalidatePath('/blog/[slug]')
return NextResponse.json({ revalidated: true, now: Date.now() })
} catch (err) {
return NextResponse.json({ message: 'Error revalidating' }, { status: 500 })
}
}
================================================
FILE: blog-app-router/app/blog/[slug]/page.js
================================================
import { getPostBySlug, getAllSlugs } from 'lib/api'
import { extractText } from 'lib/extract-text'
import { prevNextPost } from 'lib/prev-next-post'
import Container from 'components/container'
import PostHeader from 'components/post-header'
import PostBody from 'components/post-body'
import {
TwoColumn,
TwoColumnMain,
TwoColumnSidebar,
} from 'components/two-column'
import ConvertBody from 'components/convert-body'
import PostCategories from 'components/post-categories'
import Pagination from 'components/pagination'
import Image from 'next/legacy/image'
import { getPlaiceholder } from 'plaiceholder'
// ローカルの代替アイキャッチ画像
import { eyecatchLocal } from 'lib/constants'
// サイトに関する情報
import { siteMeta } from 'lib/constants'
const { siteTitle, siteUrl } = siteMeta
// ベースのメタデータ
import { openGraphMetadata, twitterMetadata } from 'lib/baseMetadata'
export default async function Post({ params }) {
const slug = params.slug
const post = await getPostBySlug(slug)
const { title, publishDate: publish, content, categories } = post
const description = extractText(content)
const eyecatch = post.eyecatch ?? eyecatchLocal
const { base64 } = await getPlaiceholder(eyecatch.url)
eyecatch.blurDataURL = base64
const allSlugs = await getAllSlugs()
const [prevPost, nextPost] = prevNextPost(allSlugs, slug)
return (
)
}
export const dynamicParams = false
export async function generateStaticParams() {
const allSlugs = await getAllSlugs()
return allSlugs.map(({ slug }) => {
return { slug: slug }
})
}
// メタデータ
export async function generateMetadata({ params }) {
const slug = params.slug
const post = await getPostBySlug(slug)
const { title: pageTitle, publishDate: publish, content, categories } = post
const pageDesc = extractText(content)
const eyecatch = post.eyecatch ?? eyecatchLocal
const ogpTitle = `${pageTitle} | ${siteTitle}`
const ogpUrl = new URL(`/blog/${slug}`, siteUrl).toString()
const metadata = {
title: pageTitle,
description: pageDesc,
openGraph: {
...openGraphMetadata,
title: ogpTitle,
description: pageDesc,
url: ogpUrl,
images: [
{
url: eyecatch.url,
width: eyecatch.width,
height: eyecatch.height,
},
],
},
twitter: {
...twitterMetadata,
title: ogpTitle,
description: pageDesc,
images: [eyecatch.url],
},
}
return metadata
}
================================================
FILE: blog-app-router/app/blog/category/[slug]/page.js
================================================
import { getAllCategories, getAllPostsByCategory } from 'lib/api'
import Container from 'components/container'
import PostHeader from 'components/post-header'
import Posts from 'components/posts'
import { getPlaiceholder } from 'plaiceholder'
// ローカルの代替アイキャッチ画像
import { eyecatchLocal } from 'lib/constants'
// サイトに関する情報
import { siteMeta } from 'lib/constants'
const { siteTitle, siteUrl } = siteMeta
// ベースのメタデータ
import { openGraphMetadata, twitterMetadata } from 'lib/baseMetadata'
export default async function Category({ params }) {
const catSlug = params.slug
const allCats = await getAllCategories()
const cat = allCats.find(({ slug }) => slug === catSlug)
const name = cat.name
const posts = await getAllPostsByCategory(cat.id)
for (const post of posts) {
if (!post.hasOwnProperty('eyecatch')) {
post.eyecatch = eyecatchLocal
}
const { base64 } = await getPlaiceholder(post.eyecatch.url)
post.eyecatch.blurDataURL = base64
}
return (
)
}
export const dynamicParams = false
export async function generateStaticParams() {
const allCats = await getAllCategories()
return allCats.map(({ slug }) => {
return { slug: slug }
})
}
// メタデータ
export async function generateMetadata({ params }) {
const catSlug = params.slug
const allCats = await getAllCategories()
const cat = allCats.find(({ slug }) => slug === catSlug)
const pageTitle = cat.name
const pageDesc = `${pageTitle}に関する記事`
const ogpTitle = `${pageTitle} | ${siteTitle}`
const ogpUrl = new URL(`/blog/category/${catSlug}`, siteUrl).toString()
const metadata = {
title: pageTitle,
description: pageDesc,
openGraph: {
...openGraphMetadata,
title: ogpTitle,
description: pageDesc,
url: ogpUrl,
},
twitter: {
...twitterMetadata,
title: ogpTitle,
description: pageDesc,
},
}
return metadata
}
================================================
FILE: blog-app-router/app/blog/page.js
================================================
import { getAllPosts } from 'lib/api'
import Container from 'components/container'
import Hero from 'components/hero'
import Posts from 'components/posts'
import { getPlaiceholder } from 'plaiceholder'
// ローカルの代替アイキャッチ画像
import { eyecatchLocal } from 'lib/constants'
// サイトに関する情報
import { siteMeta } from 'lib/constants'
const { siteTitle, siteUrl } = siteMeta
// ベースのメタデータ
import { openGraphMetadata, twitterMetadata } from 'lib/baseMetadata'
export default async function Blog() {
const posts = await getAllPosts()
for (const post of posts) {
if (!post.hasOwnProperty('eyecatch')) {
post.eyecatch = eyecatchLocal
}
const { base64 } = await getPlaiceholder(post.eyecatch.url)
post.eyecatch.blurDataURL = base64
}
return (
)
}
// メタデータ
const pageTitle = 'ブログ'
const pageDesc = 'ブログの記事一覧'
const ogpTitle = `${pageTitle} | ${siteTitle}`
const ogpUrl = new URL('/blog', siteUrl).toString()
export const metadata = {
title: pageTitle,
description: pageDesc,
openGraph: {
...openGraphMetadata,
title: ogpTitle,
description: pageDesc,
url: ogpUrl,
},
twitter: {
...twitterMetadata,
title: ogpTitle,
description: pageDesc,
},
}
================================================
FILE: blog-app-router/app/layout.js
================================================
import { Suspense } from 'react'
import {
baseMetadata,
openGraphMetadata,
twitterMetadata,
} from 'lib/baseMetadata'
import 'styles/globals.css'
import Layout from 'components/layout'
import { siteMeta } from 'lib/constants'
const { siteLang } = siteMeta
import GoogleAnalytics from 'components/googleanalytics'
// Font Awesomeの設定
import '@fortawesome/fontawesome-svg-core/styles.css'
import { config } from '@fortawesome/fontawesome-svg-core'
config.autoAddCss = false
export default function RootLayout({ children }) {
return (
{children}
)
}
export const metadata = {
...baseMetadata,
openGraph: {
...openGraphMetadata,
},
twitter: {
...twitterMetadata,
},
}
================================================
FILE: blog-app-router/app/not-found.js
================================================
import Container from 'components/container'
import Hero from 'components/hero'
export default function NotFound() {
return (
<>
404: ページが見つかりません
>
)
}
================================================
FILE: blog-app-router/app/page.js
================================================
import { getAllPosts } from 'lib/api'
import Container from 'components/container'
import Hero from 'components/hero'
import Posts from 'components/posts'
import Pagination from 'components/pagination'
import { getPlaiceholder } from 'plaiceholder'
// ローカルの代替アイキャッチ画像
import { eyecatchLocal } from 'lib/constants'
export default async function Home() {
const posts = await getAllPosts(4)
for (const post of posts) {
if (!post.hasOwnProperty('eyecatch')) {
post.eyecatch = eyecatchLocal
}
const { base64 } = await getPlaiceholder(post.eyecatch.url)
post.eyecatch.blurDataURL = base64
}
return (
)
}
================================================
FILE: blog-app-router/app/sitemap.js
================================================
// サイトに関する情報
import { siteMeta } from 'lib/constants'
const { siteUrl } = siteMeta
import { getAllSlugs, getAllCategories } from 'lib/api'
export default async function sitemap() {
// 各記事のURL
const posts = await getAllSlugs()
const postFields = posts.map((post) => {
return {
url: new URL(`/blog/${post.slug}`, siteUrl).toString(),
lastModified: new Date(),
}
})
// 各カテゴリーインデックスのURL
const cats = await getAllCategories()
const catFields = cats.map((cat) => {
return {
url: new URL(`/blog/category/${cat.slug}`, siteUrl).toString(),
lastModified: new Date(),
}
})
return [
{
url: new URL(siteUrl).toString(),
lastModified: new Date(),
},
{
url: new URL('/about', siteUrl).toString(),
lastModified: new Date(),
},
...postFields,
...catFields,
]
}
================================================
FILE: blog-app-router/components/accordion.js
================================================
'use client'
import { useState, useRef } from 'react'
import styles from 'styles/accordion.module.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCircleChevronDown } from '@fortawesome/free-solid-svg-icons'
export default function Accordion({ heading, children }) {
const [textIsOpen, setTextIsOpen] = useState(false)
const toggleText = () => {
setTextIsOpen((prev) => !prev)
}
const refText = useRef(null)
return (
)
}
================================================
FILE: blog-app-router/components/contact.js
================================================
import Social from 'components/social'
import styles from 'styles/contact.module.css'
export default function Contact() {
return (
Contact
cube@web.mail.address
)
}
================================================
FILE: blog-app-router/components/container.js
================================================
import styles from 'styles/container.module.css'
export default function Container({ children, large = false }) {
return (
{children}
)
}
================================================
FILE: blog-app-router/components/convert-body.js
================================================
import parse from 'html-react-parser'
import Image from "next/legacy/image";
export default function ConvertBody({ contentHTML }) {
const contentReact = parse(contentHTML, {
replace: (node) => {
if (node.name === 'img') {
const { src, alt, width, height } = node.attribs
return (
)
}
},
})
return <>{contentReact}>
}
================================================
FILE: blog-app-router/components/convert-date.js
================================================
import { parseISO, format } from 'date-fns'
import ja from 'date-fns/locale/ja'
export default function ConvertDate({ dateISO }) {
return (
{format(parseISO(dateISO), 'yyyy年MM月dd日', {
locale: ja,
})}
)
}
================================================
FILE: blog-app-router/components/footer.js
================================================
import Container from 'components/container'
import Logo from 'components/logo'
import Social from 'components/social'
import styles from 'styles/footer.module.css'
export default function Footer() {
return (
)
}
================================================
FILE: blog-app-router/components/googleanalytics.js
================================================
'use client'
import { useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'
import Script from 'next/script'
import * as gtag from 'lib/gtag'
function GoogleAnalytics() {
const pathname = usePathname()
const searchParams = useSearchParams()
useEffect(() => {
const url = pathname + searchParams.toString()
gtag.pageview(url)
}, [pathname, searchParams])
return (
<>
>
)
}
export default GoogleAnalytics
================================================
FILE: blog-app-router/components/header.js
================================================
import Container from 'components/container'
import Logo from 'components/logo'
import Nav from 'components/nav'
import styles from 'styles/header.module.css'
export default function Header() {
return (
)
}
================================================
FILE: blog-app-router/components/hero.js
================================================
import styles from 'styles/hero.module.css'
import Image from "next/legacy/image";
import cube from 'images/cube.jpg'
export default function Hero({ title, subtitle, imageOn = false }) {
return (
)
}
================================================
FILE: blog-app-router/components/layout.js
================================================
import Header from 'components/header'
import Footer from 'components/footer'
export default function Layout({ children }) {
return (
<>
{children}
>
)
}
================================================
FILE: blog-app-router/components/logo.js
================================================
import Link from 'next/link'
import styles from 'styles/logo.module.css'
export default function Logo({ boxOn = false }) {
return (
CUBE
);
}
================================================
FILE: blog-app-router/components/meta.js
================================================
import Head from 'next/head'
import { useRouter } from 'next/router'
// サイトに関する情報
import { siteMeta } from 'lib/constants'
const { siteTitle, siteDesc, siteUrl, siteLocale, siteType, siteIcon } =
siteMeta
// 汎用OGP画像
import siteImg from 'images/ogp.jpg'
export default function Meta({
pageTitle,
pageDesc,
pageImg,
pageImgW,
pageImgH,
}) {
// ページのタイトル
const title = pageTitle ? `${pageTitle} | ${siteTitle}` : siteTitle
// ページの説明
const desc = pageDesc ?? siteDesc
// ページのURL
const router = useRouter()
const url = `${siteUrl}${router.asPath}`
// OGP画像
const img = pageImg || siteImg.src
const imgW = pageImgW || siteImg.width
const imgH = pageImgH || siteImg.height
const imgUrl = img.startsWith('https') ? img : `${siteUrl}${img}`
return (
{title}
)
}
================================================
FILE: blog-app-router/components/nav.js
================================================
'use client'
import { useState } from 'react'
import Link from 'next/link'
import styles from 'styles/nav.module.css'
export default function Nav() {
const [navIsOpen, setNavIsOpen] = useState(false)
const toggleNav = () => {
setNavIsOpen((prev) => !prev)
}
const closeNav = () => {
setNavIsOpen(false)
}
return (
{navIsOpen && (
)}
MENU
)
}
================================================
FILE: blog-app-router/components/pagination.js
================================================
import styles from 'styles/pagination.module.css'
import Link from 'next/link'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faChevronLeft,
faChevronRight,
} from '@fortawesome/free-solid-svg-icons'
export default function Pagination({
prevText = '',
prevUrl = '',
nextText = '',
nextUrl = '',
}) {
return (
{prevText && prevUrl && (
{prevText}
)}
{nextText && nextUrl && (
{nextText}
)}
);
}
================================================
FILE: blog-app-router/components/post-body.js
================================================
import styles from 'styles/post-body.module.css'
export default function PostBody({ children }) {
return (
{children}
)
}
================================================
FILE: blog-app-router/components/post-categories.js
================================================
import styles from 'styles/post-categories.module.css'
import Link from 'next/link'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFolderOpen } from '@fortawesome/free-regular-svg-icons'
export default function PostCategories({ categories }) {
return (
Categories
{categories.map(({ name, slug }) => (
{name}
))}
);
}
================================================
FILE: blog-app-router/components/post-header.js
================================================
import styles from 'styles/post-header.module.css'
import ConvertDate from 'components/convert-date'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faClock } from '@fortawesome/free-regular-svg-icons'
export default function PostHeader({ title, subtitle, publish = '' }) {
return (
{subtitle}
{title}
{publish && (
)}
)
}
================================================
FILE: blog-app-router/components/posts.js
================================================
import styles from 'styles/posts.module.css'
import Link from 'next/link'
import Image from "next/legacy/image";
export default function Posts({ posts }) {
return (
{posts.map(({ title, slug, eyecatch }) => (
{title}
))}
);
}
================================================
FILE: blog-app-router/components/social.js
================================================
import styles from 'styles/social.module.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faTwitter,
faFacebookF,
faGithub,
} from '@fortawesome/free-brands-svg-icons'
export default function Social({ iconSize = 'initial' }) {
return (
)
}
================================================
FILE: blog-app-router/components/two-column.js
================================================
import styles from 'styles/two-column.module.css'
export function TwoColumn({ children }) {
return {children}
}
export function TwoColumnMain({ children }) {
return {children}
}
export function TwoColumnSidebar({ children }) {
return {children}
}
================================================
FILE: blog-app-router/jsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": "."
}
}
================================================
FILE: blog-app-router/lib/api.js
================================================
import { createClient } from 'microcms-js-sdk'
export const client = createClient({
serviceDomain: process.env.SERVICE_DOMAIN,
apiKey: process.env.API_KEY,
})
export async function getPostBySlug(slug) {
try {
const post = await client.get({
endpoint: 'blogs',
queries: { filters: `slug[equals]${slug}` },
})
return post.contents[0]
} catch (err) {
console.log('~~ getPostBySlug ~~')
console.log(err)
}
}
export async function getAllSlugs(limit = 100) {
try {
const slugs = await client.get({
endpoint: 'blogs',
queries: { fields: 'title,slug', orders: '-publishDate', limit: limit },
})
return slugs.contents
} catch (err) {
console.log('~~ getAllSlugs ~~')
console.log(err)
}
}
export async function getAllPosts(limit = 100) {
try {
const posts = await client.get({
endpoint: 'blogs',
queries: {
fields: 'title,slug,eyecatch',
orders: '-publishDate',
limit: limit,
},
})
return posts.contents
} catch (err) {
console.log('~~ getAllPosts ~~')
console.log(err)
}
}
export async function getAllCategories(limit = 100) {
try {
const categories = await client.get({
endpoint: 'categories',
queries: {
fields: 'name,id,slug',
limit: limit,
},
})
return categories.contents
} catch (err) {
console.log('~~ getAllCategories ~~')
console.log(err)
}
}
export async function getAllPostsByCategory(catID, limit = 100) {
try {
const posts = await client.get({
endpoint: 'blogs',
queries: {
filters: `categories[contains]${catID}`,
fields: 'title,slug,eyecatch',
orders: '-publishDate',
limit: limit,
},
})
return posts.contents
} catch (err) {
console.log('~~ getAllPostsByCategory ~~')
console.log(err)
}
}
================================================
FILE: blog-app-router/lib/baseMetadata.js
================================================
// サイトに関する情報
import { siteMeta } from 'lib/constants'
const {
siteTitle,
siteDesc,
siteLang,
siteUrl,
siteLocale,
siteType,
siteIcon,
} = siteMeta
// 汎用OGP画像
import siteImg from 'images/ogp.jpg'
// ベースとなる設定
export const baseMetadata = {
metadataBase: new URL(siteUrl),
alternates: {
canonical: './',
},
viewport: {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
},
title: {
template: `%s | ${siteTitle}`,
default: siteTitle,
},
description: siteDesc,
// icons: {
// icon: siteIcon,
// apple: siteIcon,
// },
}
// openGraphに関する設定
export const openGraphMetadata = {
title: siteTitle,
description: siteDesc,
url: siteUrl,
siteName: siteTitle,
images: [
{
url: siteImg.src,
width: siteImg.width,
height: siteImg.height,
},
],
locale: siteLocale,
type: siteType,
}
// twitterに関する設定
export const twitterMetadata = {
card: 'summary_large_image',
title: siteTitle,
description: siteDesc,
images: [siteImg.src],
}
================================================
FILE: blog-app-router/lib/constants.js
================================================
export const siteMeta = {
siteTitle: 'CUBE',
siteDesc: 'アウトプットしていくサイト',
siteUrl: 'https://*********',
siteLang: 'ja',
siteLocale: 'ja_JP',
siteType: 'website',
siteIcon: '/favicon.png',
}
export const eyecatchLocal = {
url: '/eyecatch.jpg',
width: 1920,
height: 1280,
}
================================================
FILE: blog-app-router/lib/extract-text.js
================================================
import { convert } from 'html-to-text'
export function extractText(html, length = 80, more = '…') {
const text = convert(html, {
selectors: [
{ selector: 'img', format: 'skip' },
{ selector: 'a', options: { ignoreHref: true } },
],
})
return text.slice(0, length) + more
}
================================================
FILE: blog-app-router/lib/gtag.js
================================================
export const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_ID
export const pageview = (url) => {
window.gtag('config', GA_MEASUREMENT_ID, {
page_path: url,
})
}
================================================
FILE: blog-app-router/lib/prev-next-post.js
================================================
export function prevNextPost(allSlugs, currentSlug) {
const numberOfPosts = allSlugs.length
const index = allSlugs.findIndex(({ slug }) => slug === currentSlug)
const prevPost =
index + 1 === numberOfPosts ? { title: '', slug: '' } : allSlugs[index + 1]
const nextPost = index === 0 ? { title: '', slug: '' } : allSlugs[index - 1]
return [prevPost, nextPost]
}
================================================
FILE: blog-app-router/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
// domains: ['images.microcms-assets.io'],
remotePatterns: [
{
protocol: 'https',
hostname: 'images.microcms-assets.io',
port: '',
pathname: '/assets/xxxxxxxxxx/**',
},
],
},
}
module.exports = nextConfig
================================================
FILE: blog-app-router/package.json
================================================
{
"name": "blog",
"private": true,
"description": "『作って学ぶ Next.js/React Webサイト構築』の副読本「Next.js 13 対応ガイド」の「本書のプロジェクトをApp Routerへ移行する方法」で完成させるプロジェクトデータです",
"homepage": "https://github.com/ebisucom/next-react-website",
"author": "エビスコム - EBISUCOM (https://ebisu.com/)",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-brands-svg-icons": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.1.18",
"date-fns": "^2.28.0",
"html-react-parser": "^2.0.0",
"html-to-text": "^8.2.0",
"microcms-js-sdk": "^2.0.0",
"next": "^13.4.7",
"plaiceholder": "^2.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sharp": "^0.30.7"
},
"devDependencies": {
"eslint": "^8.29.0",
"eslint-config-next": "^13.4.7"
}
}
================================================
FILE: blog-app-router/styles/accordion.module.css
================================================
.open,
.close {
border: solid 1px var(--gray-25);
}
/* 見出し */
.heading {
font-size: var(--body);
}
.heading button {
all: unset;
outline: revert;
-webkit-tap-highlight-color: transparent;
cursor: pointer;
box-sizing: border-box;
width: 100%;
padding: 1em;
display: flex;
justify-content: space-between;
gap: 1em;
}
/* 見出しのアイコン */
.icon {
color: var(--gray-25);
font-size: 1.25em;
transition: transform 0.5s;
}
.open .icon {
transform: rotate(180deg);
}
/* テキスト */
.text {
overflow: hidden;
height: 0px;
transition: none;
}
.open .text {
animation: openAnim 0.5s forwards;
}
.close .text {
animation: closeAnim 0.5s forwards;
}
@keyframes openAnim {
0% {
height: 0px;
}
99% {
height: var(--text-height);
}
100% {
height: auto;
}
}
@keyframes closeAnim {
0% {
height: var(--text-height);
}
100% {
height: 0px;
}
}
.textInner {
padding: 0 1.14em 1.14em;
font-size: calc(var(--body) * 0.875);
}
================================================
FILE: blog-app-router/styles/contact.module.css
================================================
.stack > * + * {
margin-top: var(--stack-space, 1em);
}
.heading {
font-size: var(--body);
}
================================================
FILE: blog-app-router/styles/container.module.css
================================================
.default {
width: 92%;
max-width: 1152px;
margin: 0 auto;
}
.large {
composes: default;
max-width: 1280px;
}
================================================
FILE: blog-app-router/styles/footer.module.css
================================================
.wrapper {
padding: var(--space-xl) 0;
background-color: var(--gray-10);
}
.flexContainer {
composes: sideBySideCenter from './utils.module.css';
gap: 2em;
}
================================================
FILE: blog-app-router/styles/globals.css
================================================
:root {
/* カラー(色) */
--white: #ffffff;
--gray-10: #eeeeee;
--gray-25: #aaaaaa;
--gray-50: #707070;
--gray-75: #444444;
--black: #222222;
--accent: #0d87e0;
/* タイポグラフィ(フォントサイズ) */
--body: clamp(1rem, 0.95rem + 0.2vw, 1.125rem); /* 16-18px */
--display: clamp(4.5rem, 1.83rem + 11.34vw, 10rem); /* 72-160 */
--heading1: clamp(2rem, 1.3rem + 3vw, 4rem); /* 32-64px */
--heading2: calc(var(--body) * 1.5); /* 24-27px */
--heading3: calc(var(--body) * 1.2); /* 19.2-21.6px */
--small-heading2: clamp(0.875rem, 4vw - 1rem, 1.6875rem); /* 14-27px */
--small-heading3: calc(var(--small-heading2) * 0.86); /* 12-23px */
/* スペース(余白・間隔) */
--space-xs: clamp(1.25rem, 1rem + 0.98vw, 1.875rem); /* 20-30px */
--space-sm: calc(var(--space-xs) * 1.5); /* 30-45px */
--space-md: calc(var(--space-xs) * 2); /* 40-60px */
--space-lg: calc(var(--space-xs) * 3); /* 60-90px */
--space-xl: calc(var(--space-xs) * 4); /* 80-120px */
--space-jump: clamp(1.25rem, 0.35rem + 3.8vw, 3.75rem); /* 20-60px */
}
/* 基本設定 */
body {
color: var(--black);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial,
sans-serif;
font-size: var(--body);
}
h1 {
font-size: var(--heading1);
}
h2 {
font-size: var(--heading2);
}
h3 {
font-size: var(--heading3);
}
/* next/image */
span > img {
transition: 0.2s;
}
/* リセット */
body,
h1,
h2,
h3,
p,
figure,
ul {
margin: 0;
padding: 0;
list-style: none;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
a {
color: inherit;
text-decoration: none;
}
================================================
FILE: blog-app-router/styles/header.module.css
================================================
.flexContainer {
composes: spaceBetween from 'styles/utils.module.css';
}
================================================
FILE: blog-app-router/styles/hero.module.css
================================================
.flexContainer {
composes: sideBySideCenter from 'styles/utils.module.css';
}
.text {
padding-top: calc(var(--display) * 0.5);
padding-bottom: calc(var(--display) * 0.7);
}
.title {
font-size: var(--display);
font-weight: 900;
letter-spacing: 0.15em;
}
.subtitle {
font-size: var(--small-heading2);
}
/* image */
.image {
width: 100%;
}
@media (min-width: 768px) {
.image {
width: 50%;
}
}
================================================
FILE: blog-app-router/styles/layout.module.css
================================================
================================================
FILE: blog-app-router/styles/logo.module.css
================================================
.basic {
font-size: var(--heading2);
font-weight: 700;
letter-spacing: 0.15em;
}
.box {
composes: basic;
display: inline-block;
padding: 1em 2em;
background-color: var(--gray-75);
color: var(--white);
font-size: var(--small-heading2);
}
================================================
FILE: blog-app-router/styles/nav.module.css
================================================
/* デスクトップ */
@media (min-width: 768px) {
.btn {
display: none;
}
.list {
display: flex;
gap: 2em;
}
}
/* モバイル */
@media (max-width: 767px) {
/* ボタン */
.btn {
all: unset;
outline: revert;
-webkit-tap-highlight-color: transparent;
cursor: pointer;
width: 42px;
height: 42px;
position: relative;
z-index: 200;
}
.close .btn {
color: var(--gray-75);
}
.open .btn {
color: var(--white);
}
/* ボタン内のバー */
.btn {
display: grid;
place-items: center;
}
.btn::before,
.btn::after,
.btn .bar {
grid-area: 1 / 1;
content: '';
display: block;
width: 32px;
height: 1px;
background-color: currentColor;
transition: transform 0.4s;
}
.close .btn::before {
transform: translateY(-8px);
}
.close .btn::after {
transform: translateY(8px);
}
.open .btn::before {
transform: rotate(45deg);
}
.open .btn::after {
transform: rotate(-45deg);
}
.open .btn .bar {
transform: scale(0);
}
/* メニュー(オーバーレイ) */
.list {
position: fixed;
inset: 0 -100% 0 100%;
z-index: 100;
background: rgba(0, 0, 0, 0.8);
color: var(--white);
transition: transform 0.4s;
}
.open .list {
transform: translateX(-100%);
}
/* メニュー(オーバーレイ内の配置) */
.list {
display: grid;
gap: 40px;
place-content: center;
text-align: center;
}
}
/* ホバー */
@media (hover: hover) {
.list a:hover {
color: var(--accent);
}
}
@media (hover: none) {
.list a {
-webkit-tap-highlight-color: transparent;
}
.list a:active {
color: var(--accent);
}
}
================================================
FILE: blog-app-router/styles/pagination.module.css
================================================
.flexContainer {
composes: spaceBetween from 'styles/utils.module.css';
margin: var(--space-lg) 0;
gap: 1em;
}
.next {
margin-left: auto;
}
.iconText {
display: flex;
align-items: center;
gap: 0.5em;
}
================================================
FILE: blog-app-router/styles/post-body.module.css
================================================
.stack > * + * {
margin-top: var(--stack-space, 1.5em);
}
.stack h2,
.stack h3 {
--stack-space: 2em;
}
.stack h2 + *,
.stack h3 + * {
--stack-space: 0.8em;
}
.stack p {
line-height: 1.8;
}
.stack ul {
padding: revert;
list-style: revert;
}
================================================
FILE: blog-app-router/styles/post-categories.module.css
================================================
.flexContainer {
display: flex;
align-items: baseline;
gap: 1.25rem;
color: var(--gray-50);
}
@media (min-width: 768px) {
.flexContainer {
flex-direction: column;
}
}
.heading {
font-size: var(--small-heading2);
}
.list {
composes: flexContainer;
font-size: var(--small-heading3);
gap: 0.75rem;
}
================================================
FILE: blog-app-router/styles/post-header.module.css
================================================
.stack {
padding: var(--space-sm) 0;
}
.stack > * + * {
margin-top: var(--stack-space, 1em);
}
.subtitle {
font-size: var(--small-heading2);
font-weight: 700;
}
.title {
--stack-space: 0.2em;
}
.publish {
display: flex;
gap: 0.5em;
color: var(--gray-50);
font-size: var(--small-heading3);
}
================================================
FILE: blog-app-router/styles/posts.module.css
================================================
.gridContainer {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-jump);
margin-top: var(--space-xs);
margin-bottom: var(--space-lg);
}
.post h2 {
margin-top: 0.5em;
font-size: var(--small-heading3);
font-weight: 400;
}
.post figure {
position: relative;
aspect-ratio: 16/9;
}
================================================
FILE: blog-app-router/styles/social.module.css
================================================
.list {
display: flex;
gap: 1.5em;
font-size: var(--icon-size, 24px);
}
================================================
FILE: blog-app-router/styles/two-column.module.css
================================================
.flexContainer {
composes: sideBySide from './utils.module.css';
gap: var(--space-md);
margin: var(--space-md) 0 var(--space-lg);
}
@media (min-width: 768px) {
.main {
width: 768px;
}
.sidebar {
width: 240px;
position: sticky;
top: 40px;
align-self: flex-start;
}
.sidebar * {
text-align: right;
}
.sidebar :is(div, ul) {
width: fit-content;
margin-left: auto;
place-items: flex-end;
place-content: flex-end;
}
}
================================================
FILE: blog-app-router/styles/utils.module.css
================================================
/* 両端揃え */
.spaceBetween {
display: flex;
justify-content: space-between;
align-items: center;
}
/* 横並び(基本形) */
.sideBySide {
display: flex;
flex-direction: column;
}
@media (min-width: 768px) {
.sideBySide {
flex-direction: row;
justify-content: space-between;
}
}
/* 横並び(中央揃え) */
.sideBySideCenter {
composes: sideBySide;
align-items: center;
text-align: center;
}
@media (min-width: 768px) {
.sideBySideCenter {
text-align: left;
}
}
================================================
FILE: blog-chap6/.eslintrc.json
================================================
{
"extends": "next/core-web-vitals"
}
================================================
FILE: blog-chap6/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
================================================
FILE: blog-chap6/README.md
================================================
# ブログサイトのプロジェクト
『作って学ぶ Next.js/React Webサイト構築』のChapter 6のプロジェクトデータです。
* [サポートサイト](https://book.mynavi.jp/supportsite/detail/9784839980177.html)
* [書籍情報](https://ebisu.com/next-react-website/)
================================================
FILE: blog-chap6/components/contact.js
================================================
import Social from 'components/social'
import styles from 'styles/contact.module.css'
export default function Contact() {
return (
Contact
cube@web.mail.address
)
}
================================================
FILE: blog-chap6/components/container.js
================================================
import styles from 'styles/container.module.css'
export default function Container({ children, large = false }) {
return (
{children}
)
}
================================================
FILE: blog-chap6/components/footer.js
================================================
import Container from 'components/container'
import Logo from 'components/logo'
import Social from 'components/social'
import styles from 'styles/footer.module.css'
export default function Footer() {
return (
)
}
================================================
FILE: blog-chap6/components/header.js
================================================
import Container from 'components/container'
import Logo from 'components/logo'
import Nav from 'components/nav'
import styles from 'styles/header.module.css'
export default function Header() {
return (
)
}
================================================
FILE: blog-chap6/components/hero.js
================================================
import styles from 'styles/hero.module.css'
import Image from 'next/image'
import cube from 'images/cube.jpg'
export default function Hero({ title, subtitle, imageOn = false }) {
return (
)
}
================================================
FILE: blog-chap6/components/layout.js
================================================
import Header from 'components/header'
import Footer from 'components/footer'
export default function Layout({ children }) {
return (
<>
{children}
>
)
}
================================================
FILE: blog-chap6/components/logo.js
================================================
import Link from 'next/link'
import styles from 'styles/logo.module.css'
export default function Logo({ boxOn = false }) {
return (
CUBE
)
}
================================================
FILE: blog-chap6/components/meta.js
================================================
import Head from 'next/head'
import { useRouter } from 'next/router'
// サイトに関する情報
import { siteMeta } from 'lib/constants'
const { siteTitle, siteDesc, siteUrl, siteLocale, siteType, siteIcon } = siteMeta
// 汎用OGP画像
import siteImg from 'images/ogp.jpg'
export default function Meta({ pageTitle, pageDesc, pageImg, pageImgW, pageImgH }) {
// ページのタイトル
const title = pageTitle ? `${pageTitle} | ${siteTitle}` : siteTitle
// ページの説明
const desc = pageDesc || siteDesc
// ページのURL
const router = useRouter()
const url = `${siteUrl}${router.asPath}`
// OGP画像
const img = pageImg || siteImg.src
const imgW = pageImgW || siteImg.width
const imgH = pageImgH || siteImg.height
const imgUrl = img.startsWith('https') ? img : `${siteUrl}${img}`
return (
{title}
)
}
================================================
FILE: blog-chap6/components/nav.js
================================================
import Link from 'next/link'
import styles from 'styles/nav.module.css'
export default function Nav() {
return (
)
}
================================================
FILE: blog-chap6/components/post-body.js
================================================
import styles from 'styles/post-body.module.css'
export default function PostBody({ children }) {
return (
{children}
)
}
================================================
FILE: blog-chap6/components/social.js
================================================
import styles from 'styles/social.module.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faTwitter,
faFacebookF,
faGithub,
} from '@fortawesome/free-brands-svg-icons'
export default function Social({ iconSize = 'initial' }) {
return (
)
}
================================================
FILE: blog-chap6/components/two-column.js
================================================
import styles from 'styles/two-column.module.css'
export function TwoColumn({ children }) {
return {children}
}
export function TwoColumnMain({ children }) {
return {children}
}
export function TwoColumnSidebar({ children }) {
return {children}
}
================================================
FILE: blog-chap6/jsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": "."
}
}
================================================
FILE: blog-chap6/lib/constants.js
================================================
export const siteMeta = {
siteTitle: 'CUBE',
siteDesc: 'アウトプットしていくサイト',
siteUrl: 'https://*********',
siteLang: 'ja',
siteLocale: 'ja_JP',
siteType: 'website',
siteIcon: '/favicon.png',
}
================================================
FILE: blog-chap6/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig
================================================
FILE: blog-chap6/package.json
================================================
{
"name": "blog-chap6",
"private": true,
"description": "「作って学ぶ Next.js/React Webサイト構築」のChapter 6のプロジェクトデータです",
"homepage": "https://github.com/ebisucom/next-react-website",
"author": "エビスコム - EBISUCOM (https://ebisu.com/)",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-brands-svg-icons": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.1.18",
"next": "12.2.0",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"eslint": "^8.29.0",
"eslint-config-next": "^13.0.6"
}
}
================================================
FILE: blog-chap6/pages/_app.js
================================================
import 'styles/globals.css'
import Layout from 'components/layout'
// Font Awesomeの設定
import '@fortawesome/fontawesome-svg-core/styles.css'
import { config } from '@fortawesome/fontawesome-svg-core'
config.autoAddCss = false
function MyApp({ Component, pageProps }) {
return (
)
}
export default MyApp
================================================
FILE: blog-chap6/pages/_document.js
================================================
import { Html, Head, Main, NextScript } from 'next/document'
import { siteMeta } from 'lib/constants'
const { siteLang } = siteMeta
export default function Document() {
return (
)
}
================================================
FILE: blog-chap6/pages/about.js
================================================
import Meta from 'components/meta'
import Container from 'components/container'
import Hero from 'components/hero'
import PostBody from 'components/post-body'
import Contact from 'components/contact'
import { TwoColumn, TwoColumnMain, TwoColumnSidebar } from 'components/two-column'
import Image from 'next/image'
import eyecatch from 'images/about.jpg'
export default function About() {
return (
Cubeが得意とする分野はモノづくりです。3次元から2次元の造形、プログラミングやデザインなど、さまざまな技術を組み合わせることによって社会や環境と結びつけるクリエイティブを提案し続けています。
モノづくりで目指していること
モノづくりではデータの解析からクリエイティブまで幅広いことを担当しています。新しいことを取り入れながら、ユーザーにマッチした提案を実現するのが目標です。たくさんの開発・提供が数多くありますが、特にそこを磨く作業に力を入れています。
単純に形にするだけでなく、作る過程や、なぜそのようにしたのかを大事にしながらものづくりをしています。毎回課題解決テーマをもって「モノ」と向き合い制作をし、フィードバックしてもらうことで自分の中にあるモヤモヤを言葉にして「問い」への答えを出しています。
新しいことへのチャレンジ
今までと違うものを作ることで愛着が湧いてきます。そこで興味を持ったことは小さなことでもいいから取り入れて、良いものを作れるようにしています。小さなヒントから新しいものを生み出すようなモノづくりは、これからも続けていきたいです。
)
}
================================================
FILE: blog-chap6/pages/api/hello.js
================================================
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' })
}
================================================
FILE: blog-chap6/pages/blog/index.js
================================================
import Meta from 'components/meta'
import Container from 'components/container'
import Hero from 'components/hero'
export default function Blog() {
return (
)
}
================================================
FILE: blog-chap6/pages/index.js
================================================
import Meta from 'components/meta'
import Container from 'components/container'
import Hero from 'components/hero'
export default function Home() {
return (
)
}
================================================
FILE: blog-chap6/styles/contact.module.css
================================================
.stack > * + * {
margin-top: var(--stack-space, 1em);
}
.heading {
font-size: var(--body);
}
================================================
FILE: blog-chap6/styles/container.module.css
================================================
.default {
width: 92%;
max-width: 1152px;
margin: 0 auto;
}
.large {
composes: default;
max-width: 1280px;
}
================================================
FILE: blog-chap6/styles/footer.module.css
================================================
.wrapper {
padding: var(--space-xl) 0;
background-color: var(--gray-10);
}
.flexContainer {
composes: sideBySideCenter from './utils.module.css';
gap: 2em;
}
================================================
FILE: blog-chap6/styles/globals.css
================================================
:root {
/* カラー(色) */
--white: #ffffff;
--gray-10: #eeeeee;
--gray-25: #aaaaaa;
--gray-50: #707070;
--gray-75: #444444;
--black: #222222;
--accent: #0d87e0;
/* タイポグラフィ(フォントサイズ) */
--body: clamp(1rem, 0.95rem + 0.2vw, 1.125rem); /* 16-18px */
--display: clamp(4.5rem, 1.83rem + 11.34vw, 10rem); /* 72-160 */
--heading1: clamp(2rem, 1.3rem + 3vw, 4rem); /* 32-64px */
--heading2: calc(var(--body) * 1.5); /* 24-27px */
--heading3: calc(var(--body) * 1.2); /* 19.2-21.6px */
--small-heading2: clamp(0.875rem, 4vw - 1rem, 1.6875rem); /* 14-27px */
--small-heading3: calc(var(--small-heading2) * 0.86); /* 12-23px */
/* スペース(余白・間隔) */
--space-xs: clamp(1.25rem, 1rem + 0.98vw, 1.875rem); /* 20-30px */
--space-sm: calc(var(--space-xs) * 1.5); /* 30-45px */
--space-md: calc(var(--space-xs) * 2); /* 40-60px */
--space-lg: calc(var(--space-xs) * 3); /* 60-90px */
--space-xl: calc(var(--space-xs) * 4); /* 80-120px */
--space-jump: clamp(1.25rem, 0.35rem + 3.8vw, 3.75rem); /* 20-60px */
}
/* 基本設定 */
body {
color: var(--black);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial,
sans-serif;
font-size: var(--body);
}
h1 {
font-size: var(--heading1);
}
h2 {
font-size: var(--heading2);
}
h3 {
font-size: var(--heading3);
}
/* next/image */
span > img {
transition: 0.2s;
}
/* リセット */
body,
h1,
h2,
h3,
p,
figure,
ul {
margin: 0;
padding: 0;
list-style: none;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
a {
color: inherit;
text-decoration: none;
}
================================================
FILE: blog-chap6/styles/header.module.css
================================================
.flexContainer {
composes: spaceBetween from 'styles/utils.module.css';
}
================================================
FILE: blog-chap6/styles/hero.module.css
================================================
.flexContainer {
composes: sideBySideCenter from 'styles/utils.module.css';
}
.text {
padding-top: calc(var(--display) * 0.5);
padding-bottom: calc(var(--display) * 0.7);
}
.title {
font-size: var(--display);
font-weight: 900;
letter-spacing: 0.15em;
}
.subtitle {
font-size: var(--small-heading2);
}
/* image */
.image {
width: 100%;
}
@media (min-width: 768px) {
.image {
width: 50%;
}
}
================================================
FILE: blog-chap6/styles/layout.module.css
================================================
================================================
FILE: blog-chap6/styles/logo.module.css
================================================
.basic {
font-size: var(--heading2);
font-weight: 700;
letter-spacing: 0.15em;
}
.box {
composes: basic;
display: inline-block;
padding: 1em 2em;
background-color: var(--gray-75);
color: var(--white);
font-size: var(--small-heading2);
}
================================================
FILE: blog-chap6/styles/nav.module.css
================================================
.list {
display: flex;
gap: 2em;
}
@media (hover: hover) {
.list a:hover {
color: var(--accent);
}
}
@media (hover: none) {
.list a {
-webkit-tap-highlight-color: transparent;
}
.list a:active {
color: var(--accent);
}
}
================================================
FILE: blog-chap6/styles/post-body.module.css
================================================
.stack > * + * {
margin-top: var(--stack-space, 1.5em);
}
.stack h2,
.stack h3 {
--stack-space: 2em;
}
.stack h2 + *,
.stack h3 + * {
--stack-space: 0.8em;
}
.stack p {
line-height: 1.8;
}
.stack ul {
padding: revert;
list-style: revert;
}
================================================
FILE: blog-chap6/styles/social.module.css
================================================
.list {
display: flex;
gap: 1.5em;
font-size: var(--icon-size, 24px);
}
================================================
FILE: blog-chap6/styles/two-column.module.css
================================================
.flexContainer {
composes: sideBySide from './utils.module.css';
gap: var(--space-md);
margin: var(--space-md) 0 var(--space-lg);
}
@media (min-width: 768px) {
.main {
width: 768px;
}
.sidebar {
width: 240px;
position: sticky;
top: 40px;
align-self: flex-start;
}
.sidebar * {
text-align: right;
}
.sidebar :is(div, ul) {
width: fit-content;
margin-left: auto;
place-items: flex-end;
place-content: flex-end;
}
}
================================================
FILE: blog-chap6/styles/utils.module.css
================================================
/* 両端揃え */
.spaceBetween {
display: flex;
justify-content: space-between;
align-items: center;
}
/* 横並び(基本形) */
.sideBySide {
display: flex;
flex-direction: column;
}
@media (min-width: 768px) {
.sideBySide {
flex-direction: row;
justify-content: space-between;
}
}
/* 横並び(中央揃え) */
.sideBySideCenter {
composes: sideBySide;
align-items: center;
text-align: center;
}
@media (min-width: 768px) {
.sideBySideCenter {
text-align: left;
}
}
================================================
FILE: images-local/README.md
================================================
# 画像データ
## Pixabayの画像
* [Pixabay](https://pixabay.com/)
* [Pixabay License](https://pixabay.com/service/license/)
### cube.jpg
https://pixabay.com/images/id-2137085
### about.jpg
https://pixabay.com/images/id-7072224
### eyecatch.jpg
https://pixabay.com/images/id-3084014
---
## その他の画像
エビスコムが作成した画像です。
* favicon.ico
* favicon.png
* ogp.jpg
================================================
FILE: images-local/README.txt
================================================
# 画像データ
## Pixabayの画像
* [Pixabay](https://pixabay.com/)
* [Pixabay License](https://pixabay.com/service/license/)
### cube.jpg
https://pixabay.com/images/id-2137085
### about.jpg
https://pixabay.com/images/id-7072224
### eyecatch.jpg
https://pixabay.com/images/id-3084014
---
## その他の画像
エビスコムが作成した画像です。
* favicon.ico
* favicon.png
* ogp.jpg
================================================
FILE: images-post/README.md
================================================
# 画像データ
## Pixabayの画像
* [Pixabay](https://pixabay.com/)
* [Pixabay License](https://pixabay.com/service/license/)
### art-of-reading.jpg
https://pixabay.com/images/id-6858948
### calc.jpg
https://pixabay.com/images/id-4855963
### cat.jpg
https://pixabay.com/images/id-6108829
### check.jpg
https://pixabay.com/images/id-77426
### clouds.jpg
https://pixabay.com/images/id-6151744
### heart.jpg
https://pixabay.com/images/id-5728469
### interior.jpg
https://pixabay.com/images/id-2700712
### kamakura.jpg
https://pixabay.com/images/id-5540113
### music.jpg
https://pixabay.com/images/id-3084012
### pattern.jpg
https://pixabay.com/images/id-3048876
### recipe.jpg
https://pixabay.com/images/id-4393918
### rocket.jpg
https://pixabay.com/images/id-6721486
### room-temp.jpg
https://pixabay.com/images/id-2700671
### schedule.jpg
https://pixabay.com/images/id-3191241
### wheat.jpg
https://pixabay.com/images/id-4931280
================================================
FILE: images-post/README.txt
================================================
# 画像データ
## Pixabayの画像
* [Pixabay](https://pixabay.com/)
* [Pixabay License](https://pixabay.com/service/license/)
### art-of-reading.jpg
https://pixabay.com/images/id-6858948
### calc.jpg
https://pixabay.com/images/id-4855963
### cat.jpg
https://pixabay.com/images/id-6108829
### check.jpg
https://pixabay.com/images/id-77426
### clouds.jpg
https://pixabay.com/images/id-6151744
### heart.jpg
https://pixabay.com/images/id-5728469
### interior.jpg
https://pixabay.com/images/id-2700712
### kamakura.jpg
https://pixabay.com/images/id-5540113
### music.jpg
https://pixabay.com/images/id-3084012
### pattern.jpg
https://pixabay.com/images/id-3048876
### recipe.jpg
https://pixabay.com/images/id-4393918
### rocket.jpg
https://pixabay.com/images/id-6721486
### room-temp.jpg
https://pixabay.com/images/id-2700671
### schedule.jpg
https://pixabay.com/images/id-3191241
### wheat.jpg
https://pixabay.com/images/id-4931280
================================================
FILE: import/api-blogs.json
================================================
{"apiFields":[{"idValue":"kr887emSVX","fieldId":"title","name":"タイトル","kind":"text","required":true,"isUnique":false},{"fieldId":"slug","name":"スラッグ","kind":"text","required":true,"isUnique":true},{"fieldId":"publishDate","name":"投稿日","kind":"date","required":true},{"fieldId":"content","name":"内容","kind":"richEditor","required":true,"richEditorMultiParagraph":true,"richEditorImageSize":true},{"fieldId":"eyecatch","name":"アイキャッチ","kind":"media"},{"fieldId":"categories","name":"カテゴリ","kind":"relationList","required":true,"referenceDisplayItem":"FlRaIsFU55"}],"customFields":[]}
================================================
FILE: import/api-categories.json
================================================
{"apiFields":[{"idValue":"FlRaIsFU55","fieldId":"name","name":"カテゴリ名","kind":"text","required":true,"isUnique":false},{"fieldId":"slug","name":"スラッグ","kind":"text","required":true,"isUnique":true}],"customFields":[]}
================================================
FILE: import/contents-blogs.csv
================================================
"コンテンツID
※空欄で構いません。特定の値を設定したい場合に入力してください。",title,slug,publishDate,content,eyecatch,categories
,スケジュール管理と猫の理論,schedule,2022-05-07T01:00:00.000Z,"何でもすぐに忘れてしまうので、予定を忘れないようにスケジュール管理手帳で予定を管理しています。でも、本当はスケジュールをスケジュールとして正しく認識できていないのかもしれません。
スケジュール管理は猫にまかせろ?
スケジュール管理も1つのデザインです。スケジュール管理の理論として有名な本もあります。『仕事と生活のバランス』で話題の著者は、学問的基礎に基づく仕事と家庭の両立の方法論の形成をめざし、仕事と生活の両立に有用なソーシャルメディアの使い方、ダイバーシティマネジメント、フレックスタイム制度の使い方などに言及しています。
「難しいことは猫にまかせろ」という言葉があるように、自分で把握する方法もありますが、誰かに把握してもらう方法もあります。自分ひとりで完璧に把握しようとすると、必要な内容以外まで管理しはじめるので、余分な時間や労力を使うからです。
なんだか難しく感じますが、ポイントは「時間と場所と目的を忘れないこと」が大前提だと思います。どうせなら楽しく、しっかりスケジュール管理ができるようになりたいものです。",(インポート非対応です),"design,fun"
,音楽が呼び起こす美味しいものの記憶,music,2022-05-03T05:00:00.000Z,"懐かしい音楽をきくと、そのときに体験した喜怒哀楽がよみがえります。大人になってから懐かしさを懐かしむことって結構多いと思うんですが、よく知らない昔を懐かしむよりも、音楽で振り返った方がより懐かしさが自分の中に残りやすい気がします。
そのことが、また誰かに話したいと思うきっかけになることもあります。私にとって、音楽によって呼び起こされる記憶はたこ焼きソースの味です。美味しいですよね、たこ焼き。なぜ買ってきてくれたのかは解らないけど、音楽とセットで覚えていて、とにかく大好きな食べ物になりました。おいしさの旋律とでも言うのでしょうか。
脳科学的にも、感覚器や視覚、聴覚、味覚といった感覚が伴って記憶されていることはわかっているみたいです。ただ、いくら優れた記憶力を持っている人でも、日々の生活の中で蓄積した記憶はフラッシュバックされることはないもの。そんなことから、新鮮な音楽には脳に刺激を与え、リフレッシュするための機能があり、感覚が伴って記憶されていると考えられているようです。
それが個人的にはたこ焼きなわけですが…。たこ焼き味のせんべいも美味しいでよね。記事を書いていたら久しぶりに食べたくなってきました。",(インポート非対応です),"technology,fun"
,カメラが捉えるミクロの世界,micro,2022-05-01T02:00:00.000Z,"今世紀に入ってからテクノロジーが加速度的に進化して、これまで見えなかった世界が見えてきました。その傾向は加速化する一方で、もう止められないのかもしれません。
スマホで手軽に写真が撮れるようになったことも拍車をかけているようです。カメラに搭載されている画像処理エンジンが高速化した事もあり、細かい設定も簡単にできるようになってきています。写真の加工をするなら高性能なアプリもあります。
その1つが、カメラが捉えるミクロの世界です。ミクロの世界は沢山の人々に感動を与え続けています。そして、ミクロの世界を世界に写して、人間のその脳の領分の中でどう情報として抽出して処理するか、その発想はこのデジタル世界の最初期の発想のような気がします。
ミクロの世界を写して見えるものを見つめて、マクロの世界を理解する。さらに、そこには人間の知力では判らない様々な側面、立体性、抽象性の世界が広がっているということが最近の研究から分かってきています。人間は脳を介してしか世界の像を捉えることができない、それがカメラのようなものだとしたら・・・と仮説を立てた最新の研究です。
こうした研究がやがて身近なスマホなどに反映され、生活に溶け込んでいくのかもしれません。",(インポート非対応です),technology
,かわいい雲のオブジェ,clouds,2022-04-27T00:00:00.000Z,"モクモクした雲は、やっぱり好きです。木漏れ日の下でそんな雲を眺めていたら、自分もすっかり別世界に紛れ込んでしまったような気分になれます。
などと言いながら、かわいい雲のオブジェ制作にチャレンジしてみました。ポイントは「モクモクした感じ」です。
最初はリアルな雲を再現しようとしたのですが、やはり4Kディスプレイでなければなかなか難しいのではないかと思ってしまいました。恐らく、「リアルな雲を再現!」等の文字が入っていても実際に見るとがっかりすることになるかもしれません。
流体シミュレーションを用いてアニメーションを自由自在に動かすソフトも使ってみました。3Dプリンタでも似たようなことをやってくれるととてもありがたいんですが、そこまでやってくれるのかどうか。ちなみに、記事を書いてるうちに自分も欲しくなってきたんですが、さすがに買えません。デモンストレーション映像を見ていたら欲しくなってきたのですが…。
できあがったら雲は出ていましたが、パラパラと雨も降らずに無事終了。夕方には帰路に着きました。",(インポート非対応です),"design,fun"
,計算された美しさというものについて,calc,2022-04-23T00:00:00.000Z,"計算された自然界の美しさを再現する試みは、自然そのままの色とはかけ離れた結果となるかも知れません。しかし、コンピューターグラフィックスを使用する作業により、私たちはより美しく鮮やかになった自然を体験できるようになります。
なぜならば、スクリーン上に表示されるコンテンツが「ここが自然界と同じ色です」と伝達してくれるようになるからです。それは、私たちの日常生活の空間にも言えることです。
自宅の庭に遊びに来た小さなリスは、公園を走り回っている子リスのように、はっきり色づくことはありません。しかし、コンピューターグラフィックスの世界では、ピクセル化された色を自然に感じることができます。
さらに、イメージを拡大したり縮小したりすることで、一部分の色の境界がはっきりしない自然界の風景が再現できるだけでなく、実際の生活では不可能な鮮やかな風景までも再現できます。実際、デジタル変換された画像は、ほとんどの場合ぼやけています。しかし、それらを人の目で確認すると、その輪郭や厚み、そして奥行を強調するデジタル処理が行われ、元の画像よりリアルに感じられるため、人々はそれを驚異的だと感じます。
コンピューターによる画像処理による情報の取得により、視覚的な精度はさらに向上し、同じ画像を鑑賞するにも、よりリアルに感じられるようになると言われているのです。",(インポート非対応です),"technology,design"
,カマクラとテーブルの制作,kamakura,2022-04-17T00:00:00.000Z,"今年の冬はカマクラ作りにチャレンジする機会がありました。雪を積み上げるイグルーに対し、カマクラは雪をくり抜いて作ります。人数が多かったので、カマクラはすぐに完成しました。
大変だったのはテーブルです。テーブルの作り方は、まず8つのパーツのうち、どこをどのような形状にするかの計画を立てます。それから、材料を用意します。材料の準備ができたら順番にカットし、組み立て作業に入ります。組み立てが完了したら、最終的な調整と塗装です。
最終調整は、下塗りと上塗り、最後の乾燥の工程です。基本的に、塗料はあらかじめ用意されているものを使用し、塗料は乾かして初めて完成となります。塗装には、専用の塗料を自作する方法もあります。それでも、最初に揃える材料はDIY用パーツ販売店より安く済みます。
もっとも手軽なのは、「ハンダごて」ではないでしょうか。この他にも、塗料を混ぜる際に入れるエアブラシとマニキュアの塗料を専用に作る方法もあります。いずれにしても、他の種類の塗料に比べて塗料代は若干高くなりますが、安く済ませたい場合は手作り方法のほうが良いでしょう。
工作のときにはハンダ、マニキュアといった専用の道具が必要で、それらはホームセンターや工具の量販店などで購入することができます。また、工具や材料がなくても、カッターナイフやハサミ、ペンチを使えば作業できるでしょう。室温が低いと硬化までの時間が長くなるので、冬場の作業では要注意です。",(インポート非対応です),fun
,地図とGPSと立体と,rocket,2022-04-16T00:00:00.000Z,"知らない場所でもGPSを使った地図があれば自分の居場所を簡単に特定することができて、迷わずに済みます。
ただし、地図が読めると言ってもそれを立体的にイメージできるわけではありません。3D化する機能もありますが、それとは異なるのです。そのため、知らない土地、たとえば観光地を巡りながら道を覚えていくことになります。
そもそも地図を平面と捉えてはいけないと教わったこともあります。観光地では地図以外にも様々な情報を手に入れることができるからです。たとえば、SNSのタイムラインや天気情報もそうです。そういうことを考えていくと、地図だけで観光地を巡るのは当たり前ではないと言っても過言ではありません。
もっとも、最近の地図アプリはそうした情報も複合的に地図に取り込んでいますので、どんどん便利になってきていると言えるでしょう。",(インポート非対応です),technology
,あちこちで見かけるハートマーク,heart,2022-04-13T00:00:00.000Z,"街の中を見渡してみると、あちらこちらでハートマークが使われてます。ハートマークを見ると、何かの拍子にほっこりとした気持ちになりますね。皆さんの中にも、通勤途中で見た景色がハートの形をしていたら思わず微笑んでしまう、なんてことがあるのではないでしょうか?
よく見ると、半分ハート、3分の2ハート、2分の1ハート、なんていう風に、いろんな形のハートが上手に配置されています。ちょっと見にくくなるのですが、上のハートはハート型の絵で、下のハートはハート型のハート型という風に、かなり細かく使い分けられています。
全体を見渡すと、実は、結構いろいろなところでハートが使われていることがわかります。こんなとこにもあったの? と、意外な場所にもあったりします。
公共的な場所にも意外と多く使われていて、あちらことらでしっかりとハートマークが使われているのです。",(インポート非対応です),design
,ニューラルコンピュータとパターン,pattern,2022-04-16T00:00:00.000Z,"パターンの美しさはもちろん、あらゆるモノの中に隠された隠された秘密を一緒に見いだす冒険、それがアート&デザインのジャンルで育まれたパターンの醍醐味です。さまざまなデザインで使われているパターンやモチーフは、人間の認知パターンや感情に働きかけ、イメージによって情報が処理される過程を活性化します。
たとえば、おなじみのポインセチアによく使われるリンゴや、最近では日本の人気アニメがマスコットキャラクターとして使われ、さまざまなシーンで定着したタンポポもその一例です。文字による伝達だけではないさまざまな表現が、人間の心理的な印象に大きく作用するということの表れでしょう。
パターンにはきわめてパーソナルな表現としての面と、パブリックで広範な利益に資するシステムの両面が求められます。そして、これをまっとうすることができたからこそ、エコール・ド・パリの画家たちと同様にパターンを深く愛好する文化をつくりあげたのではないかと考えています。
言語学・考古学・音楽学といったさまざまな分野の中でもパターンが活かされています。パターンを知ることで、わたしたちの認識のあり方がどのように変わるのか、その視点から学ぶのが数学の応用分野であり、ニューラルコンピュータはまさにその対象となるというわけです。",(インポート非対応です),"technology,design"
,センサーを使った室温のコントロール,room-temp,2022-04-15T00:00:00.000Z,"快適な室温をキープするためには、エアコンやストーブをつけたり、消したりして調整しなければなりません。ただ、家電本体だけのセンサーでは無駄が多く、光熱費もかさんでしまいます。
そこで、家全体の温度センサーでいつでも快適な室温をキープできるようにしてみました。センサーを使って各部屋の室温を監視し、その値に基づいてLEDを点滅させる仕組みとなっています。これは、24時間、常時運転してもそれほど費用も電気代もかかりません。
また、部屋ごとの温度を調節するために、快適な室温は「23°C」に設定しました。 夏はともかく、冬は寒く感じることがあるからです。 一番いいのは、設定温度を2°Cほど高くすることなんですが、電気代も節約できます。
寒くなる部屋だけ設定温度を高くすることもできます。部屋全体を温めることは出来ませんが、窓を少し開けておく方法もあります。設置場所が冬の日中でも非常に暖かい場合は、消灯させる必要もあるでしょう。このように、最小限の操作で常に快適な温度に保っておくことができるのです。",(インポート非対応です),technology
,インテリアの配置とバランス,interior,2022-04-12T00:00:00.000Z,"インテリアのバランスとは「それらを一体的に統括する総合力のこと」と言うのだとか。部屋の各々の要素がバランス良く配置されていて、その内部が美しくまとまっていると、それら全体が美しく見えるということのようです。
自由な暮らし方にとって、それらをどこに配置するのか、どこからがどこの部分なのか、人の目に触れてどのように映るのか、そういうものが生活の場への気配りと心地よさを創り出します。
たとえば、ダイニングテーブルはL字型に配置するより、コの字型やL字型で囲うようにレイアウトしたほうが良い印象になります。 L字のように横に広がって配置するなら、幅広のダイニングテーブルよりも、幅の狭いものをL字の近くに配置するとバランスがとれます。
家具に余計なものを置くことで、見えてしまってはいけないものが見えてしまったり、見えないために見過ごされてしまったり、またモノを置いたら更にその圧迫感や窮屈感を与えてしまったり…。モノとの距離感というのは普段の暮らしには難しいものですが、家具選びにおいてはその距離感についても慎重になることが重要です。
ただ、その過程で必要以上に自分の部屋内のテイストを似せてしまうと、部屋の統一感を欠いてしまうので、できるだけ似たテイストにならないようなインテリア選びを心掛けたいところです。",(インポート非対応です),design
,甘いものといえば小麦,wheat,2022-04-08T00:00:00.000Z,"仕事の合間に、疲れたときは甘いものが欲しくなるものです。そんなときに食べるものを思い返してみると、小麦で作られたものが多いことに気付きます。
小麦を使って焼いたクッキー(こちらは皮ごと使ったサブレ)は少し硬めの食感で、バターの香りがとても豊か。小麦の味をダイレクトに感じられます。また、チョコレートは濃すぎず軽すぎずのちょうどいい軽さでお気に入りです。
現代のように、麦を挽いて使うようになったのは近代に入ってからです。麦が「パン作りの必須アイテム」と呼ばれるようになったのはもっと近年かもしれません。ほかの麦に比べると、水につけておくと柔らかく、水につけずに挽いて作られたものは噛みごたえが足りないといわれています。そのため、もともと水につけて食べる「穀物の副食」だった麦を「主食」として食べるようになったそうです。
ハード系・クリームパンなど、パンの種類も実にさまざま。お子様向けのパンケーキやベーグルなどのお食事系のパン、ハード系の素朴な味わいもいいですよね。温かいサンドイッチなど温かい料理を作り、温かいスープと一緒に食べることもできます。",(インポート非対応です),fun
,わからないものを読み解く技術,art-of-reading,2022-04-01T00:00:00.000Z,"よくわからなくても読み解かなくてはならない、そんな事態に直面することがあります。「言語はいくつもの道具から成る。言語の構成要素とその利用、つまり、シンボル、記号の記号化の過程は、道具とその組合せで、すなわち、文字、音、シンボル、記号で作られる。」というやつです。
たとえば、古代語の解読とプログラミングは、現代ギリシア文字の解読よりも難解といわれています。それは、アリストテレスやユークリッドが書いたという「想像力」や「知恵」や「詩的な本質」という言葉の意味と、現代数学がどのように世界を解明しようとしているかとも関連しており、おもしろいヒントを与えてくれます。
プログラムコードを読み解くことは難しそうに思えますが、データ構造体のアドレスと、ロード命令の位置は対応しています。つまり、座標系が違うだけで、同じようなプログラムが記述できることが分かります。
初心に戻ってやり直してみると、必ずまず、最初の数行を読むことからやり直さないと意味がわからなくなってしまうのですが…。その先入観を消すことが大事なのかもしれません。",(インポート非対応です),technology
,テクノロジーのレシピ,recipe,2022-03-24T00:00:00.000Z,"テクノロジーがどのように作用し、どのように機能するかには、日常の生活パターンが反映されません。そのような法則は存在しないので、多くの場合、人々が何に反応するかを理解しているかどうかによって決まります。
そのため、日常の生活パターンを参考にしつつ、ユーザーエクスペリエンスを改善することは困難なのです。では、どのようにすれば、そうしたテクノロジーをうまく活用することができるのでしょうか。
より良いユーザーエクスペリエンスが「ユーザの生活の質を高めてくれるもの」だとすれば、その観点から設計されたデザインもまた、より望ましいユーザーエクスペリエンスであると考えることができます。ユーザーエクスペリエンスは、さまざまな人間のニーズが組み合わさって形成されます。したがって、何が改善に役立つかは、数多くの取り組みから得られた知見をすべてミックスして統合することでしか、決定できません。
改善するためのデザインは、最新のコンテンツマーケティングの革新にとって不可欠な要素です。この分野の発展は、人間中心設計の設計手法にもあてはまる、さらなる変化を生み出す一助にもなるでしょう。",(インポート非対応です),technology
,チェック柄の深い歴史,check,2022-03-21T00:00:00.000Z,"今日はチェック柄のファッションをあちこちで目にしたので、その歴史を考察してみました。
チェック柄の歴史の背景には、紀元前4000年までさかのぼるナイル川流域での自然の変化が関係しているようです。ナイル川の中流部付近だった古代ではこのような激しい変化はあまり見られませんでしたが、上流部にはナツメヤシの樹やトナカイなどの動物が生息していました。
その後、ナイル川の下流や上流部で草原や牧草地が広がり、ナイル川周辺の自然環境に変化が生じました。この時代に、ナイル川やその流域などに暮らしていた先住民族の言語であるナイル語から、「チェック柄」という用語が派生したと考えられているのだとか。その後、文化によって変化したアイテムの形状や模様が、何世紀にもわたって使われています。
最近では、服だけでなく、家具や小物、持ち物までもがカムフラージュカラーで作られ、鮮やかなチェック柄でデコレーションされているのも珍しくありません。 柔らかくソフトなソファーや、インテリアにあしらってもお洒落とのこと。
まだまだ知らないことがたくさんあります。",(インポート非対応です),design
================================================
FILE: import/contents-categories.csv
================================================
"コンテンツID
※空欄で構いません。特定の値を設定したい場合に入力してください。",name,categorySlug
technology,テクノロジー,technology
design,デザインと設計,design
fun,楽しいものいろいろ,fun