├── src ├── js │ ├── polyfill.common.js │ ├── polyfill.module.js │ ├── main.js │ ├── polyfill.nomodule.js │ ├── Disclosure.js │ └── mediaQuery.js ├── html │ ├── index.yaml │ ├── _data │ │ ├── navigation.yml │ │ ├── meta.yml │ │ └── mediaQuery.yml │ ├── _includes │ │ ├── SiteFooter.pug │ │ ├── PageHeader.pug │ │ ├── SiteHeader.pug │ │ └── head.pug │ ├── license.pug │ ├── _extends │ │ └── default.pug │ ├── license.js │ └── index.pug └── css │ ├── components │ ├── _VisuallyHidden.scss │ ├── _SiteFooter.scss │ ├── _Container.scss │ ├── _PageHeader.scss │ ├── _Disclosure.scss │ └── _SiteHeader.scss │ ├── main.scss │ ├── _global.scss │ └── _base.scss ├── .gitattributes ├── vendor-public └── common.css ├── .prettierignore ├── config ├── flag.js ├── siteGenerator │ ├── settings.js │ ├── index.js │ └── task.js ├── path.js └── webpack.config.js ├── public ├── favicon.ico └── assets │ ├── ogp.png │ └── logo.svg ├── gulp-tasks ├── browserSync │ └── instance.js ├── clean.js ├── html.js ├── build.js ├── copy.js ├── watch.js ├── start.js ├── serve.js ├── js.js └── css.js ├── gulpfile.js ├── lib ├── fs.js ├── siteGenerator │ ├── index.js │ ├── buildFiles.js │ ├── config.js │ └── buildCompileMiddleware.js └── path.js ├── .editorconfig ├── .prettierrc ├── LICENSE ├── .gitignore ├── package.json └── README.md /src/js/polyfill.common.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/js/polyfill.module.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /vendor-public/common.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 80%; 3 | } 4 | -------------------------------------------------------------------------------- /src/html/index.yaml: -------------------------------------------------------------------------------- 1 | description: 静的ウェブサイトを開発するためのボイラープレートです。 2 | -------------------------------------------------------------------------------- /src/html/_data/navigation.yml: -------------------------------------------------------------------------------- 1 | siteHeader: 2 | - [ライセンス, /license.html] 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.tmp/ 2 | /dist/ 3 | /public/ 4 | /vendor-public/ 5 | package.json 6 | -------------------------------------------------------------------------------- /src/html/_includes/SiteFooter.pug: -------------------------------------------------------------------------------- 1 | footer.SiteFooter 2 | .Container MIT License 3 | -------------------------------------------------------------------------------- /src/css/components/_VisuallyHidden.scss: -------------------------------------------------------------------------------- 1 | .VisuallyHidden { 2 | @include visually-hidden; 3 | } 4 | -------------------------------------------------------------------------------- /config/flag.js: -------------------------------------------------------------------------------- 1 | const isDev = !process.argv.includes('--prod') 2 | 3 | module.exports = { 4 | isDev, 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuheiy/real-world-website-boilerplate/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/assets/ogp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuheiy/real-world-website-boilerplate/HEAD/public/assets/ogp.png -------------------------------------------------------------------------------- /config/siteGenerator/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | inputDir: 'src/html', 3 | sharedLocalDir: 'src/html/_data', 4 | } 5 | -------------------------------------------------------------------------------- /src/html/_data/meta.yml: -------------------------------------------------------------------------------- 1 | siteTitle: Real world website boilerplate 2 | lang: ja 3 | region: JP 4 | origin: https://example.com 5 | -------------------------------------------------------------------------------- /src/css/components/_SiteFooter.scss: -------------------------------------------------------------------------------- 1 | .SiteFooter { 2 | padding-top: 1rem; 3 | padding-bottom: 1rem; 4 | text-align: center; 5 | } 6 | -------------------------------------------------------------------------------- /src/html/_data/mediaQuery.yml: -------------------------------------------------------------------------------- 1 | sm: "(min-width: 576px)" 2 | md: "(min-width: 768px)" 3 | lg: "(min-width: 992px)" 4 | xl: "(min-width: 1200px)" 5 | -------------------------------------------------------------------------------- /gulp-tasks/browserSync/instance.js: -------------------------------------------------------------------------------- 1 | const browserSync = require('browser-sync') 2 | 3 | const bs = browserSync.create() 4 | 5 | module.exports = bs 6 | -------------------------------------------------------------------------------- /src/html/_includes/PageHeader.pug: -------------------------------------------------------------------------------- 1 | header.PageHeader 2 | h1.PageHeader__heading= page.title || data.meta.siteTitle 3 | p.PageHeader__lede= page.description 4 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const start = require('./gulp-tasks/start') 2 | const build = require('./gulp-tasks/build') 3 | 4 | exports.default = start 5 | exports.build = build 6 | -------------------------------------------------------------------------------- /lib/fs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const util = require('util') 3 | 4 | const readFileAsync = util.promisify(fs.readFile) 5 | 6 | module.exports = { 7 | readFileAsync, 8 | } 9 | -------------------------------------------------------------------------------- /gulp-tasks/clean.js: -------------------------------------------------------------------------------- 1 | const del = require('del') 2 | const { destDir } = require('../config/path') 3 | 4 | const clean = () => { 5 | return del(destDir) 6 | } 7 | 8 | module.exports = clean 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /src/css/main.scss: -------------------------------------------------------------------------------- 1 | // Global 2 | @import "global"; 3 | 4 | // Base 5 | @import "../../node_modules/normalize.css/normalize"; 6 | @import "base"; 7 | 8 | // Component 9 | @import "components/**/*.scss"; 10 | -------------------------------------------------------------------------------- /lib/siteGenerator/index.js: -------------------------------------------------------------------------------- 1 | const buildFiles = require('./buildFiles') 2 | const buildCompileMiddleware = require('./buildCompileMiddleware') 3 | 4 | module.exports = { 5 | buildFiles, 6 | buildCompileMiddleware, 7 | } 8 | -------------------------------------------------------------------------------- /src/html/license.pug: -------------------------------------------------------------------------------- 1 | extends /_extends/default 2 | 3 | block body 4 | include /_includes/PageHeader 5 | 6 | section(lang="en") 7 | h2= page.licenseContent.split('\n')[0] 8 | pre= page.licenseContent.split('\n').slice(2).join('\n') 9 | -------------------------------------------------------------------------------- /gulp-tasks/html.js: -------------------------------------------------------------------------------- 1 | const { buildFiles } = require('../lib/siteGenerator') 2 | const siteGeneratorConfig = require('../config/siteGenerator') 3 | 4 | const html = () => { 5 | return buildFiles(siteGeneratorConfig) 6 | } 7 | 8 | module.exports = html 9 | -------------------------------------------------------------------------------- /src/html/_extends/default.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang=data.meta.lang) 3 | include /_includes/head 4 | 5 | body 6 | include /_includes/SiteHeader 7 | 8 | main.Container 9 | block body 10 | 11 | include /_includes/SiteFooter 12 | -------------------------------------------------------------------------------- /src/css/components/_Container.scss: -------------------------------------------------------------------------------- 1 | .Container { 2 | max-width: 60rem; 3 | margin-right: auto; 4 | margin-left: auto; 5 | padding-right: 1rem; 6 | padding-left: 1rem; 7 | 8 | @media #{$mq-md} { 9 | padding-right: 2rem; 10 | padding-left: 2rem; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | import './polyfill.common' 2 | import Disclosure from './Disclosure' 3 | 4 | console.log({ 5 | __DEV__: __DEV__, 6 | __BASE_PATH__: __BASE_PATH__, 7 | }) 8 | 9 | document.querySelectorAll('.Disclosure').forEach((root) => { 10 | Disclosure(root) 11 | }) 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "arrowParens": "always", 4 | "semi": false, 5 | "singleQuote": true, 6 | "overrides": [ 7 | { 8 | "files": ["*.scss", "*.yaml"], 9 | "options": { 10 | "singleQuote": false 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/css/components/_PageHeader.scss: -------------------------------------------------------------------------------- 1 | .PageHeader { 2 | padding-top: 2rem; 3 | padding-bottom: 2rem; 4 | text-align: center; 5 | } 6 | 7 | .PageHeader__heading { 8 | margin-top: 0; 9 | margin-bottom: 0; 10 | } 11 | 12 | .PageHeader__lede { 13 | margin-top: 1rem; 14 | margin-bottom: 0; 15 | } 16 | -------------------------------------------------------------------------------- /gulp-tasks/build.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const clean = require('./clean') 3 | const html = require('./html') 4 | const css = require('./css') 5 | const js = require('./js') 6 | const copy = require('./copy') 7 | 8 | const build = gulp.series(clean, gulp.parallel(html, css, js, copy)) 9 | 10 | module.exports = build 11 | -------------------------------------------------------------------------------- /lib/path.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const toPOSIXPath = (filePath) => { 4 | if (path.sep === path.posix.sep) { 5 | return filePath 6 | } 7 | 8 | return filePath.replace( 9 | new RegExp(`\\${path.win32.sep}`, 'g'), 10 | path.posix.sep, 11 | ) 12 | } 13 | 14 | module.exports = { 15 | toPOSIXPath, 16 | } 17 | -------------------------------------------------------------------------------- /src/html/license.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const { readFileAsync } = require('../../lib/fs') 3 | 4 | module.exports = async () => { 5 | return { 6 | title: 'ライセンス', 7 | description: '本プロジェクトはMITライセンスです。', 8 | licenseContent: await readFileAsync( 9 | join(__dirname, '../../LICENSE'), 10 | 'utf8', 11 | ), 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/siteGenerator/buildFiles.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const withConfig = require('./config') 3 | 4 | const logError = (err) => { 5 | console.error(String(err)) 6 | } 7 | 8 | const buildFiles = withConfig((config) => { 9 | return config 10 | .task(gulp.src(config.vinylInput), logError) 11 | .pipe(gulp.dest(config.outputDir)) 12 | }) 13 | 14 | module.exports = buildFiles 15 | -------------------------------------------------------------------------------- /config/siteGenerator/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { toPOSIXPath } = require('../../lib/path') 3 | const { basePath, destBaseDir } = require('../path') 4 | const { inputDir } = require('./settings') 5 | const task = require('./task') 6 | 7 | module.exports = { 8 | inputDir, 9 | inputExt: '.pug', 10 | outputDir: destBaseDir, 11 | outputExt: '.html', 12 | task, 13 | basePath: toPOSIXPath(path.join('/', basePath)), 14 | } 15 | -------------------------------------------------------------------------------- /gulp-tasks/copy.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const gulp = require('gulp') 3 | const { toPOSIXPath } = require('../lib/path') 4 | const { destBaseDir, publicDir } = require('../config/path') 5 | 6 | const copy = () => { 7 | return gulp 8 | .src(toPOSIXPath(path.join(publicDir, '**')), { 9 | dot: true, 10 | since: gulp.lastRun(copy), 11 | }) 12 | .pipe(gulp.dest(destBaseDir)) 13 | } 14 | 15 | module.exports = copy 16 | -------------------------------------------------------------------------------- /src/css/components/_Disclosure.scss: -------------------------------------------------------------------------------- 1 | .Disclosure { 2 | } 3 | 4 | .Disclosure__summary { 5 | } 6 | 7 | .Disclosure__toggle { 8 | padding: 0.25rem 0.75rem; 9 | background-color: hsl(0, 0%, 95%); 10 | border: 1px solid hsla(0, 0%, 0%, 0.1); 11 | border-radius: 2px; 12 | box-shadow: 0 1px 2px 0 hsla(0, 0%, 0%, 0.2); 13 | } 14 | 15 | .Disclosure__toggle:active { 16 | background-color: hsl(0, 0%, 85%); 17 | } 18 | 19 | .Disclosure__details { 20 | } 21 | -------------------------------------------------------------------------------- /src/js/polyfill.nomodule.js: -------------------------------------------------------------------------------- 1 | // import 'picturefill' 2 | import 'element-closest' 3 | 4 | // https://github.com/zloirock/core-js/issues/329 5 | // https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach 6 | if (window.NodeList && !NodeList.prototype.forEach) { 7 | NodeList.prototype.forEach = function(callback, thisArg) { 8 | thisArg = thisArg || window 9 | for (var i = 0; i < this.length; i++) { 10 | callback.call(thisArg, this[i], i, this) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/js/Disclosure.js: -------------------------------------------------------------------------------- 1 | const Disclosure = (root) => { 2 | const toggle = root.querySelector('.Disclosure__toggle') 3 | const details = root.querySelector('.Disclosure__details') 4 | let isExpanded = false 5 | 6 | toggle.addEventListener('click', () => { 7 | const isNextExpanded = !isExpanded 8 | 9 | toggle.setAttribute('aria-expanded', isNextExpanded) 10 | details.hidden = !isNextExpanded 11 | 12 | isExpanded = isNextExpanded 13 | }) 14 | } 15 | 16 | export default Disclosure 17 | -------------------------------------------------------------------------------- /gulp-tasks/watch.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const { publicDir, vendorPublicDir } = require('../config/path') 3 | const bs = require('./browserSync/instance') 4 | const css = require('./css') 5 | 6 | const makeReload = (...args) => { 7 | return function reload(done) { 8 | bs.reload(...args) 9 | done() 10 | } 11 | } 12 | 13 | const opts = { 14 | delay: 0, 15 | } 16 | 17 | const watch = (done) => { 18 | gulp.watch(['src/html', publicDir, vendorPublicDir], makeReload(), opts) 19 | gulp.watch('src/css', gulp.series(css, makeReload('*.css')), opts) 20 | done() 21 | } 22 | 23 | module.exports = watch 24 | -------------------------------------------------------------------------------- /src/css/_global.scss: -------------------------------------------------------------------------------- 1 | $font-size-browser-default: 16px; 2 | 3 | // Media query 4 | // Use em unit for bugs in Safari. 5 | // https://zellwk.com/blog/media-query-units/ 6 | $mq-sm: "(min-width: 45em)"; 7 | $mq-md: "(min-width: 60em)"; 8 | $mq-lg: "(min-width: 75em)"; 9 | 10 | // Screen reader 11 | @mixin visually-hidden { 12 | position: absolute !important; 13 | overflow: hidden !important; 14 | width: 1px !important; 15 | height: 1px !important; 16 | padding: 0 !important; 17 | border: 0 !important; 18 | white-space: nowrap !important; 19 | clip-path: inset(50%) !important; 20 | clip: rect(0, 0, 0, 0) !important; // deprecated 21 | } 22 | -------------------------------------------------------------------------------- /gulp-tasks/start.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const bs = require('./browserSync/instance') 3 | const clean = require('./clean') 4 | const css = require('./css') 5 | const js = require('./js') 6 | const watch = require('./watch') 7 | const serve = require('./serve') 8 | 9 | const enableWatchMode = (done) => { 10 | css.enableWatchMode() 11 | js.enableWatchMode((hasErrors) => { 12 | if (hasErrors) { 13 | return 14 | } 15 | 16 | bs.reload('*.js') 17 | }) 18 | done() 19 | } 20 | 21 | const start = gulp.series( 22 | clean, 23 | enableWatchMode, 24 | gulp.parallel(css, js), 25 | watch, 26 | serve, 27 | ) 28 | 29 | module.exports = start 30 | -------------------------------------------------------------------------------- /config/path.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const parseArgs = require('minimist') 3 | const { isDev } = require('./flag') 4 | 5 | const argv = parseArgs(process.argv.slice(2)) 6 | 7 | const basePath = argv.subdir || '' 8 | const assetsPath = path.join(basePath, 'assets') 9 | 10 | const destDir = isDev ? '.tmp' : 'dist' 11 | const destBaseDir = path.join(destDir, basePath) 12 | const destAssetsDir = path.join(destDir, assetsPath) 13 | 14 | const publicDir = 'public' 15 | const vendorPublicDir = 'vendor-public' 16 | 17 | module.exports = { 18 | basePath, 19 | assetsPath, 20 | destDir, 21 | destBaseDir, 22 | destAssetsDir, 23 | publicDir, 24 | vendorPublicDir, 25 | } 26 | -------------------------------------------------------------------------------- /src/html/_includes/SiteHeader.pug: -------------------------------------------------------------------------------- 1 | header.SiteHeader 2 | .Container 3 | nav.SiteHeader__nav 4 | ul.SiteHeader__navList 5 | - const _SiteHeader_isHomeCurrent = '/' === page.path 6 | li.SiteHeader__navItem 7 | a.SiteHeader__navLink(class={'-current': _SiteHeader_isHomeCurrent} href=absPath('/') aria-current=_SiteHeader_isHomeCurrent && 'page') 8 | = data.meta.siteTitle 9 | span.VisuallyHidden ホーム 10 | each link in data.navigation.siteHeader 11 | - const isCurrent = link[1] === page.path 12 | li.SiteHeader__navItem 13 | a.SiteHeader__navLink(class={'-current': isCurrent} href=absPath(link[1]) aria-current=isCurrent && 'page')= link[0] 14 | -------------------------------------------------------------------------------- /src/css/components/_SiteHeader.scss: -------------------------------------------------------------------------------- 1 | .SiteHeader { 2 | padding-top: 1rem; 3 | padding-bottom: 1rem; 4 | } 5 | 6 | .SiteHeader__navList { 7 | display: flex; 8 | flex-direction: column; 9 | align-items: flex-end; 10 | margin-top: 0; 11 | margin-bottom: 0; 12 | padding-left: 0; 13 | list-style-type: none; 14 | 15 | @media #{$mq-sm} { 16 | flex-direction: row; 17 | } 18 | } 19 | 20 | .SiteHeader__navItem { 21 | } 22 | 23 | .SiteHeader__navItem:first-child { 24 | @media #{$mq-sm} { 25 | flex-grow: 1; 26 | } 27 | } 28 | 29 | .SiteHeader__navItem + .SiteHeader__navItem { 30 | margin-top: 0.5rem; 31 | 32 | @media #{$mq-sm} { 33 | margin-top: 0; 34 | margin-left: 1.25rem; 35 | } 36 | } 37 | 38 | .SiteHeader__navLink { 39 | } 40 | 41 | .SiteHeader__navLink.-current { 42 | font-weight: bold; 43 | } 44 | -------------------------------------------------------------------------------- /src/js/mediaQuery.js: -------------------------------------------------------------------------------- 1 | const BROWSER_DEFAULT_FONT_SIZE = 16 2 | 3 | export const MEDIA_QUERY_SMALL = `(min-width: ${576 / 4 | BROWSER_DEFAULT_FONT_SIZE}em)` 5 | export const MEDIA_QUERY_MEDIUM = `(min-width: ${768 / 6 | BROWSER_DEFAULT_FONT_SIZE}em)` 7 | export const MEDIA_QUERY_LARGE = `(min-width: ${992 / 8 | BROWSER_DEFAULT_FONT_SIZE}em)` 9 | export const MEDIA_QUERY_XLARGE = `(min-width: ${1200 / 10 | BROWSER_DEFAULT_FONT_SIZE}em)` 11 | 12 | // Extended version of 13 | // https://github.com/Polymer/pwa-helpers/blob/5cb02bacb6c6f72ceafeb1345de669e9986fcf70/src/media-query.ts 14 | export const installMediaQueryWatcher = (mediaQuery, layoutChangedCallback) => { 15 | const mql = window.matchMedia(mediaQuery) 16 | const listener = (ev) => { 17 | layoutChangedCallback(ev.matches) 18 | } 19 | mql.addListener(listener) 20 | layoutChangedCallback(mql.matches) 21 | const uninstall = () => { 22 | mql.removeListener(listener) 23 | } 24 | return uninstall 25 | } 26 | -------------------------------------------------------------------------------- /gulp-tasks/serve.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { toPOSIXPath } = require('../lib/path') 3 | const { buildCompileMiddleware } = require('../lib/siteGenerator') 4 | const { 5 | basePath, 6 | destDir, 7 | publicDir, 8 | vendorPublicDir, 9 | } = require('../config/path') 10 | const siteGeneratorConfig = require('../config/siteGenerator') 11 | const bs = require('./browserSync/instance') 12 | 13 | const compileHtmlMiddleware = buildCompileMiddleware(siteGeneratorConfig) 14 | 15 | const serve = (done) => { 16 | bs.init( 17 | { 18 | notify: false, 19 | ui: false, 20 | server: { 21 | baseDir: [vendorPublicDir, destDir], 22 | routes: { 23 | [toPOSIXPath(path.join('/', basePath))]: publicDir, 24 | }, 25 | }, 26 | middleware: [compileHtmlMiddleware], 27 | startPath: toPOSIXPath(path.join('/', basePath, '/')), 28 | ghostMode: false, 29 | open: false, 30 | }, 31 | done, 32 | ) 33 | } 34 | 35 | module.exports = serve 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Yuhei Yasuda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/css/_base.scss: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | line-height: 1.5; 4 | word-wrap: break-word; 5 | overflow-wrap: break-word; 6 | text-underline-position: under; 7 | } 8 | 9 | address, 10 | em, 11 | cite, 12 | dfn, 13 | var, 14 | i { 15 | font-style: inherit; 16 | } 17 | 18 | img, 19 | iframe, 20 | video, 21 | audio, 22 | svg, 23 | canvas { 24 | vertical-align: middle; 25 | } 26 | 27 | img, 28 | video { 29 | max-width: 100%; 30 | height: auto; 31 | } 32 | 33 | svg { 34 | fill: currentcolor; 35 | } 36 | 37 | table { 38 | border-collapse: collapse; 39 | } 40 | 41 | th { 42 | text-align: left; 43 | } 44 | 45 | input, 46 | button, 47 | select, 48 | optgroup, 49 | textarea { 50 | padding: 0; 51 | font: inherit; 52 | color: inherit; 53 | background-color: transparent; 54 | border: 0; 55 | } 56 | 57 | select { 58 | border-radius: 0; 59 | -webkit-appearance: none; 60 | -moz-appearance: none; 61 | } 62 | 63 | select::-ms-expand { 64 | display: none; 65 | } 66 | 67 | fieldset { 68 | min-width: 0; 69 | margin: 0; 70 | padding: 0; 71 | border: 0; 72 | } 73 | 74 | [hidden] { 75 | display: none !important; 76 | } 77 | -------------------------------------------------------------------------------- /gulp-tasks/js.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const webpackConfig = require('../config/webpack.config') 3 | 4 | let isWatchMode = false 5 | let watchCallback = null 6 | 7 | const js = (done) => { 8 | const compiler = webpack(webpackConfig) 9 | let isFirst = true 10 | 11 | const callback = (err, stats) => { 12 | if (err) { 13 | throw err 14 | } 15 | 16 | console.log( 17 | stats.toString({ 18 | colors: true, 19 | modules: false, 20 | version: false, 21 | }), 22 | ) 23 | 24 | if (stats.hasErrors()) { 25 | if (!isWatchMode) { 26 | done(new Error('webpack compilation errors')) 27 | return 28 | } 29 | } 30 | 31 | if (isFirst) { 32 | done() 33 | isFirst = false 34 | return 35 | } 36 | 37 | if (watchCallback) { 38 | watchCallback(stats.hasErrors()) 39 | } 40 | } 41 | 42 | if (isWatchMode) { 43 | compiler.watch({}, callback) 44 | } else { 45 | compiler.run(callback) 46 | } 47 | } 48 | 49 | js.enableWatchMode = (callback) => { 50 | isWatchMode = true 51 | watchCallback = callback 52 | } 53 | 54 | module.exports = js 55 | -------------------------------------------------------------------------------- /src/html/_includes/head.pug: -------------------------------------------------------------------------------- 1 | head(prefix="og: http://ogp.me/ns#") 2 | meta(charset="utf-8") 3 | meta(name="viewport" content="width=device-width, initial-scale=1") 4 | if page.title 5 | title= `${page.title} - ${data.meta.siteTitle}` 6 | else 7 | title= data.meta.siteTitle 8 | meta(name="description" content=page.description) 9 | meta(name="twitter:card" content="summary_large_image") 10 | meta(property="og:title" content=page.title || data.meta.siteTitle) 11 | meta(property="og:type" content="website") 12 | meta(property="og:image" content=`${data.meta.origin}${assetPath('ogp.png')}`) 13 | meta(property="og:url" content=`${data.meta.origin}${absPath(page.path)}`) 14 | meta(property="og:description" content=page.description) 15 | meta(property="og:site_name" content=data.meta.siteTitle) 16 | meta(property="og:locale" content=`${data.meta.lang}_${data.meta.region}`) 17 | link(rel="canonical" href=absPath(page.path)) 18 | if absPath() !== '/' 19 | link(rel="icon" type="image/x-icon" href=absPath('favicon.ico')) 20 | link(rel="stylesheet" href=assetPath('main.bundle.css')) 21 | script(type="module" src=assetPath('main.module.bundle.js')) 22 | script(nomodule src=assetPath('main.nomodule.bundle.js') defer) 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # project build output 64 | /dist/ 65 | /.tmp/ 66 | -------------------------------------------------------------------------------- /gulp-tasks/css.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const rename = require('gulp-rename') 3 | const sass = require('gulp-sass') 4 | const globImporter = require('node-sass-glob-importer') 5 | const postcss = require('gulp-postcss') 6 | const autoprefixer = require('autoprefixer') 7 | const gapProperties = require('postcss-gap-properties') 8 | const csswring = require('csswring') 9 | const { isDev } = require('../config/flag') 10 | const { destAssetsDir } = require('../config/path') 11 | 12 | const sassImporters = [globImporter()] 13 | 14 | const postcssPlugins = [ 15 | autoprefixer({ 16 | cascade: false, 17 | grid: 'autoplace', 18 | }), 19 | gapProperties({ preserve: false }), 20 | !isDev && csswring(), 21 | ].filter(Boolean) 22 | 23 | let isWatchMode = false 24 | 25 | const css = () => { 26 | const sassStream = sass({ importer: sassImporters }) 27 | 28 | if (isWatchMode) { 29 | sassStream.on('error', sass.logError) 30 | } 31 | 32 | return gulp 33 | .src(['src/css/*.scss', '!src/css/_*.scss'], { sourcemaps: isDev }) 34 | .pipe(rename({ suffix: '.bundle' })) 35 | .pipe(sassStream) 36 | .pipe(postcss(postcssPlugins)) 37 | .pipe(gulp.dest(destAssetsDir, { sourcemaps: isDev && '.' })) 38 | } 39 | 40 | css.enableWatchMode = () => { 41 | isWatchMode = true 42 | } 43 | 44 | module.exports = css 45 | -------------------------------------------------------------------------------- /src/html/index.pug: -------------------------------------------------------------------------------- 1 | extends /_extends/default 2 | 3 | block body 4 | include /_includes/PageHeader 5 | 6 | section 7 | h2 導入 8 | p よそは十一月人知れずその学習性に従って方の中に云っないまし。もっとも十月が抑圧通りはもうこの講演たなけれでもから聞きて下さっなけれには推薦握っだだろて、実際にはするませうだます。権力を掘なのはまるで今がむしろないますです。単に嘉納さんに発見手本とても納得をしない国この主義私か仕事にといったお運動ですたたませて、その今日もあなたか下国家が愛するて、嘉納君ののから気の毒のあなたが近頃ご学習とすみて君本位でご保留が傾けるようにまあ小意味を握ったんから、どうしてももし発音に考えんて来ます事がもっなけれです。それでつまりご漫然にする気はまだ高等と申し上げたて、その菓子にはしましてという秋刀魚で解らてしまっですなく。 9 | figure 10 | a(href="https://yuheiy.com/" rel="author") 11 | img(src=assetPath('logo.svg') width="256" height="256" alt="yuhei") 12 | figcaption プロジェクトの所有者です。 13 | p この限り一口の時その差はそれ中を当てうかと岡田さんがもっなあり、人の途中たというおお話ですたうて、釣のためが主義を今日ほどの後を結果するて行くから、そうの今がありゃからそんなためをようやく減ったですとするましのでて、ありがたいだたからそうお魚籃できんものたんた。ただ正義か高等か反抗に罹っだから、今日ごろ文学を防ぐてかねるまし中にお批評の十一月が勤まりますだ。時間をはましてあっから罹っんませですまして、とうていじっと炙って料理はぴたりないんのです。つまりお専攻がなっのにはいるたのたば、男には、もし私かいうてしれでしょなするられるですなくともって、心はするば行くまいあり。もし毫ももほとんど弊害といういでて、私をは時分上ぐらいそれのご交渉もないしいたませ。 14 | 15 | .Disclosure(role="group") 16 | .Disclosure__summary 17 | button.Disclosure__toggle(type="button" aria-expanded="false") ディスクロージャ 18 | .Disclosure__details(hidden) 19 | p 私もどうぞ煩悶ののがごお話は起るがいるなたたらまして、十十の一団に全く出ますという[#「ますて、しかしその壇の魂をありれと、何かを私の壇上で学習が云っているですのたろたと干渉なるて学習しいだませ。 20 | p 腹の中にもしくは嘉納さんをただどうさでのましんた。三宅さんはそれほど心が出からなれですものましなけれです。 21 | p (さて字にする日たですなとじはできるたございて、)再びやっつけます国で、everyの文芸ともするで引きって、人の尊重も今のためくらい掘りやまはずにあろたて教育者引込んばみたとして同人うものた。あなたもどうか人が至っですように間違っていた方らしくがただ全く目黒雨云いですた。 22 | -------------------------------------------------------------------------------- /lib/siteGenerator/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const minimatch = require('minimatch') 3 | const replaceExt = require('replace-ext') 4 | const { toPOSIXPath } = require('../path') 5 | 6 | const loadConfig = (opts = {}) => { 7 | const inputDir = opts.inputDir || 'src' 8 | const inputExt = opts.inputExt || '.html' 9 | const exclude = opts.exclude || ['**/_*', '**/_*/**'] 10 | const outputDir = opts.outputDir || 'dist' 11 | const outputExt = opts.outputExt || '.html' 12 | const task = opts.task || ((stream, _handleError) => stream) 13 | const basePath = opts.basePath || '/' 14 | 15 | return { 16 | inputDir, 17 | inputExt, 18 | exclude, 19 | outputDir, 20 | outputExt, 21 | task, 22 | basePath, 23 | get vinylInput() { 24 | const inputPattern = toPOSIXPath( 25 | path.join(inputDir, '**', `*${inputExt}`), 26 | ) 27 | const excludePatterns = exclude.map((pattern) => { 28 | return `!${pattern}` 29 | }) 30 | return [inputPattern, ...excludePatterns] 31 | }, 32 | isExcludes: (pathname) => { 33 | const inputPathFromInputDir = path.relative(inputDir, pathname) 34 | return exclude.some((pattern) => { 35 | return minimatch(inputPathFromInputDir, pattern) 36 | }) 37 | }, 38 | getInputPath: (outputPath) => { 39 | if (path.extname(outputPath) !== outputExt) { 40 | return null 41 | } 42 | 43 | const inputPathFromInputDir = replaceExt(outputPath, inputExt) 44 | const inputPath = path.join(inputDir, inputPathFromInputDir) 45 | return inputPath 46 | }, 47 | } 48 | } 49 | 50 | const withConfig = (cb) => (opts) => { 51 | const config = loadConfig(opts) 52 | return cb(config) 53 | } 54 | 55 | module.exports = withConfig 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "gulp", 5 | "build": "gulp build --prod" 6 | }, 7 | "dependencies": { 8 | "@babel/polyfill": "^7.0.0", 9 | "element-closest": "^2.0.2", 10 | "normalize.css": "^8.0.1", 11 | "picturefill": "^3.0.3" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.2.2", 15 | "@babel/plugin-proposal-class-properties": "^7.2.1", 16 | "@babel/plugin-proposal-object-rest-spread": "^7.2.0", 17 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 18 | "@babel/preset-env": "^7.2.0", 19 | "autoprefixer": "^9.4.3", 20 | "babel-loader": "8.0.4", 21 | "browser-sync": "^2.26.3", 22 | "csswring": "^7.0.0", 23 | "del": "^3.0.0", 24 | "fast-glob": "^2.2.4", 25 | "gulp": "^4.0.0", 26 | "gulp-data": "^1.3.1", 27 | "gulp-postcss": "^8.0.0", 28 | "gulp-pug": "^4.0.1", 29 | "gulp-rename": "^1.4.0", 30 | "gulp-sass": "^4.0.2", 31 | "husky": "^1.3.0", 32 | "import-fresh": "^2.0.0", 33 | "js-yaml": "^3.12.0", 34 | "license-banner-webpack-plugin": "^2.1.1", 35 | "lodash.clonedeep": "^4.5.0", 36 | "lodash.uniqby": "^4.7.0", 37 | "mime": "^2.4.0", 38 | "minimatch": "^3.0.4", 39 | "minimist": "^1.2.0", 40 | "node-sass-glob-importer": "^5.2.0", 41 | "plugin-error": "^1.0.1", 42 | "postcss-gap-properties": "^2.0.0", 43 | "prettier": "^1.15.3", 44 | "pretty-quick": "^1.8.0", 45 | "replace-ext": "^1.0.0", 46 | "through2": "^3.0.0", 47 | "webpack": "^4.27.1" 48 | }, 49 | "engines": { 50 | "node": ">=8.9.0" 51 | }, 52 | "browserslist": [ 53 | "last 1 version", 54 | "> 1% in JP", 55 | "not dead" 56 | ], 57 | "husky": { 58 | "hooks": { 59 | "pre-commit": "pretty-quick --staged" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /public/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const LicenseBannerPlugin = require('license-banner-webpack-plugin') 4 | const pkg = require('../package.json') 5 | const { toPOSIXPath } = require('../lib/path') 6 | const { isDev } = require('./flag') 7 | const { basePath, assetsPath, destAssetsDir } = require('./path') 8 | 9 | const rootDir = path.join(__dirname, '..') 10 | 11 | module.exports = ['module', 'nomodule'].map((type) => { 12 | const isTypeModule = type === 'module' 13 | 14 | return { 15 | mode: isDev ? 'development' : 'production', 16 | context: rootDir, 17 | entry: [`./src/js/polyfill.${type}.js`, './src/js/main.js'], 18 | output: { 19 | path: path.join(rootDir, destAssetsDir), 20 | filename: `[name].${type}.bundle.js`, 21 | publicPath: toPOSIXPath(path.join('/', assetsPath, '/')), 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.js$/, 27 | include: path.join(rootDir, 'src/js'), 28 | use: { 29 | loader: 'babel-loader', 30 | options: { 31 | babelrc: false, 32 | presets: [ 33 | [ 34 | '@babel/preset-env', 35 | { 36 | targets: isTypeModule 37 | ? { 38 | esmodules: true, 39 | } 40 | : { 41 | // Googlebot uses a web rendering service that is based on Chrome 41. 42 | // https://developers.google.com/search/docs/guides/rendering 43 | browsers: [...pkg.browserslist, 'Chrome 41'], 44 | }, 45 | useBuiltIns: 'usage', 46 | }, 47 | ], 48 | ], 49 | plugins: [ 50 | '@babel/plugin-proposal-class-properties', 51 | '@babel/plugin-proposal-object-rest-spread', 52 | '@babel/plugin-syntax-dynamic-import', 53 | ], 54 | cacheDirectory: true, 55 | }, 56 | }, 57 | }, 58 | ], 59 | }, 60 | devtool: isDev && 'cheap-module-source-map', 61 | plugins: [ 62 | new webpack.DefinePlugin({ 63 | __DEV__: isDev, 64 | __BASE_PATH__: JSON.stringify(toPOSIXPath(path.join('/', basePath))), 65 | }), 66 | !isDev && 67 | new LicenseBannerPlugin({ 68 | licenseDirectories: [path.join(rootDir, 'node_modules')], 69 | }), 70 | ].filter(Boolean), 71 | } 72 | }) 73 | -------------------------------------------------------------------------------- /config/siteGenerator/task.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const importFresh = require('import-fresh') 3 | const cloneDeep = require('lodash.clonedeep') 4 | const parseYaml = require('js-yaml').safeLoad 5 | const PluginError = require('plugin-error') 6 | const uniqBy = require('lodash.uniqby') 7 | const fg = require('fast-glob') 8 | const replaceExt = require('replace-ext') 9 | const pug = require('gulp-pug') 10 | const data = require('gulp-data') 11 | const { toPOSIXPath } = require('../../lib/path') 12 | const { readFileAsync } = require('../../lib/fs') 13 | const { isDev } = require('../flag') 14 | const { basePath, assetsPath } = require('../path') 15 | const { inputDir, sharedLocalDir } = require('./settings') 16 | 17 | const defaultLocals = { 18 | absPath: (filePath = '') => toPOSIXPath(path.join('/', basePath, filePath)), 19 | assetPath: (filePath = '') => 20 | toPOSIXPath(path.join('/', assetsPath, filePath)), 21 | __DEV__: isDev, 22 | } 23 | 24 | const readFile = async (filePath) => { 25 | return { 26 | path: filePath, 27 | contents: await readFileAsync(filePath, 'utf8'), 28 | } 29 | } 30 | 31 | const readFiles = async (filePaths) => { 32 | const tasks = filePaths.map(readFile) 33 | return await Promise.all(tasks) 34 | } 35 | 36 | const parsers = new Map() 37 | parsers.set('.js', async (file) => { 38 | const importedModule = importFresh(file.path) 39 | const value = 40 | typeof importedModule === 'function' 41 | ? await importedModule(defaultLocals) 42 | : importedModule 43 | return cloneDeep(value) 44 | }) 45 | parsers.set('.yml', (file) => parseYaml(file.contents)) 46 | parsers.set('.yaml', parsers.get('.yml')) 47 | parsers.set('.json', (file) => JSON.parse(file.contents)) 48 | 49 | const localFileExts = [...parsers.keys()] 50 | 51 | const parseFile = async (file) => { 52 | try { 53 | const { name, ext } = path.parse(file.path) 54 | const parse = parsers.get(ext) 55 | const value = await parse(file) 56 | 57 | return { 58 | key: name, 59 | value, 60 | } 61 | } catch (err) { 62 | const pluginError = new PluginError('parseFile', err, { 63 | fileName: file.path, 64 | }) 65 | throw pluginError 66 | } 67 | } 68 | 69 | const parseFiles = async (files) => { 70 | const tasks = files.map(parseFile) 71 | return await Promise.all(tasks) 72 | } 73 | 74 | const readDataLocals = async () => { 75 | const localFilePaths = uniqBy( 76 | await fg(localFileExts.map((ext) => path.join(sharedLocalDir, `*${ext}`)), { 77 | absolute: true, 78 | }), 79 | (filePath) => path.parse(filePath).name, 80 | ) 81 | const files = await parseFiles(await readFiles(localFilePaths)) 82 | const dataLocals = files.reduce((memo, { key, value }) => { 83 | memo[key] = value 84 | return memo 85 | }, {}) 86 | return dataLocals 87 | } 88 | 89 | const readPageLocals = async (filePath) => { 90 | const [localFilePath] = await fg( 91 | localFileExts.map((ext) => replaceExt(filePath, ext)), 92 | ) 93 | 94 | let file 95 | if (localFilePath) { 96 | file = await parseFile(await readFile(localFilePath)) 97 | 98 | if (typeof file.value !== 'object') { 99 | throw new TypeError( 100 | `${localFilePath} must export an object or a function that returns an object`, 101 | ) 102 | } 103 | } else { 104 | file = { 105 | value: null, 106 | } 107 | } 108 | 109 | const filePathFromInputDir = path.relative(path.resolve(inputDir), filePath) 110 | const pagePath = toPOSIXPath( 111 | path.join('/', replaceExt(filePathFromInputDir, '.html')), 112 | ).replace(/\/index\.html$/, '/') 113 | const pageLocals = { 114 | ...file.value, 115 | path: pagePath, 116 | } 117 | return pageLocals 118 | } 119 | 120 | const readLocals = async (file) => { 121 | return { 122 | ...defaultLocals, 123 | data: await readDataLocals(), 124 | page: await readPageLocals(file.path), 125 | } 126 | } 127 | 128 | const pugOpts = { 129 | basedir: inputDir, 130 | } 131 | 132 | const task = (stream, handleError) => { 133 | return stream 134 | .pipe(data(readLocals)) 135 | .on('error', handleError) 136 | .pipe(pug(pugOpts)) 137 | .on('error', handleError) 138 | } 139 | 140 | module.exports = task 141 | -------------------------------------------------------------------------------- /lib/siteGenerator/buildCompileMiddleware.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const URL = require('url').URL 4 | const PluginError = require('plugin-error') 5 | const mime = require('mime') 6 | const through = require('through2') 7 | const gulp = require('gulp') 8 | const { toPOSIXPath } = require('../path') 9 | const withConfig = require('./config') 10 | 11 | const isDirectoryPath = (pathname) => { 12 | return pathname.endsWith('/') 13 | } 14 | 15 | const normalizePath = (pathname) => { 16 | if (isDirectoryPath(pathname)) { 17 | return toPOSIXPath(path.join(pathname, 'index.html')) 18 | } 19 | return pathname 20 | } 21 | 22 | const resolveBase = (base, target) => { 23 | const loop = (basePaths, targetPaths) => { 24 | const baseHead = basePaths[0] 25 | if (!baseHead) { 26 | return toPOSIXPath(path.join('/', ...targetPaths)) 27 | } 28 | 29 | const targetHead = targetPaths[0] 30 | if (baseHead !== targetHead) { 31 | return null 32 | } 33 | 34 | return loop(basePaths.slice(1), targetPaths.slice(1)) 35 | } 36 | 37 | const basePaths = base.split('/').filter((item) => item !== '') 38 | const targetPaths = target.split('/').filter((item) => item !== '') 39 | return loop(basePaths, targetPaths) 40 | } 41 | 42 | const escapeHtml = (str) => { 43 | if (str == null) { 44 | return '' 45 | } 46 | 47 | return String(str) 48 | .replace(/&/g, '&') 49 | .replace(//g, '>') 51 | .replace(/"/g, '"') 52 | .replace(/'/g, ''') 53 | } 54 | 55 | const wrapErrorWithHtml = (err) => { 56 | const escapedName = escapeHtml(err.name) 57 | const escapedMessage = escapeHtml(err.message) 58 | 59 | return ` 60 | 61 | 62 | 63 | 64 | ${escapedName} in plugin ${err.plugin} 65 | 70 | 71 | 72 |

${escapedName} in plugin ${err.plugin}

73 |
${escapedMessage}
74 | 75 | 76 | ` 77 | } 78 | 79 | const buildCompileMiddleware = withConfig((config) => { 80 | const transform = (req, res, next) => { 81 | const parsedPath = new URL(req.url, 'http://example.com').pathname 82 | const requestedPath = normalizePath(parsedPath) 83 | 84 | const outputPath = resolveBase(config.basePath, requestedPath) 85 | if (!outputPath) { 86 | next() 87 | return 88 | } 89 | 90 | const inputPath = config.getInputPath(outputPath) 91 | if (!inputPath) { 92 | next() 93 | return 94 | } 95 | 96 | if (!fs.existsSync(inputPath)) { 97 | next() 98 | return 99 | } 100 | 101 | if (config.isExcludes(inputPath)) { 102 | next() 103 | return 104 | } 105 | 106 | const responseError = (err) => { 107 | res.statusCode = 500 108 | res.setHeader('Content-Type', 'text/html') 109 | res.end(wrapErrorWithHtml(err)) 110 | } 111 | 112 | const handleError = (err) => { 113 | console.error(String(err)) 114 | responseError(err) 115 | } 116 | 117 | const responseFile = through.obj((file, _enc, cb) => { 118 | if (file.isNull()) { 119 | const err = new PluginError( 120 | 'responseFile', 121 | new TypeError('file is not nullable'), 122 | ) 123 | cb(err) 124 | return 125 | } 126 | 127 | if (file.isStream()) { 128 | const err = new PluginError( 129 | 'responseFile', 130 | new TypeError('Streaming not supported'), 131 | ) 132 | cb(err) 133 | return 134 | } 135 | 136 | res.setHeader('Content-Type', mime.getType(outputPath)) 137 | res.end(file.contents) 138 | cb(null) 139 | }) 140 | 141 | config 142 | .task(gulp.src(inputPath), handleError) 143 | .pipe(responseFile) 144 | .on('error', handleError) 145 | } 146 | 147 | return transform 148 | }) 149 | 150 | module.exports = buildCompileMiddleware 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Real world website boilerplate 2 | 3 | 静的ウェブサイトを開発するためのボイラープレートです。大量の HTML ファイルも快適に開発できる仕組みに注力しつつ、モダンな開発ツールを採用しながらも柔軟な構成になっています。 4 | 5 | ## 目次 6 | 7 | - [推奨環境](#推奨環境) 8 | - [インストール](#インストール) 9 | - [開発用サーバー](#開発用サーバー) 10 | - [本番用ビルド](#本番用ビルド) 11 | - [ディレクトリ構成](#ディレクトリ構成) 12 | - [`src/`](#src) 13 | - [`src/html/`](#srchtml) 14 | - [`src/css/`](#srccss) 15 | - [`src/js/`](#srcjs) 16 | - [`public/`](#public) 17 | - [`vendor-public/`](#vendor-public) 18 | - [`dist/`](#dist) 19 | - [`.tmp/`](#tmp) 20 | - [HTML テンプレート](#html-テンプレート) 21 | - [データファイル](#データファイル) 22 | - [変数](#変数) 23 | - [`data`](#data) 24 | - [`page`](#page) 25 | - [`page.path`](#pagepath) 26 | - [`absPath(filePath)`](#abspathfilepath) 27 | - [`assetPath(filePath)`](#assetpathfilepath) 28 | - [`__DEV__`](#__dev__) 29 | - [設定](#設定) 30 | - [サブディレクトリ](#サブディレクトリ) 31 | - [アセットディレクトリ](#アセットディレクトリ) 32 | - [独自フラグ](#独自フラグ) 33 | - [Server Side Includes](#server-side-includes) 34 | - [テンプレートエンジン](#テンプレートエンジン) 35 | - [HTML の整形](#html-の整形) 36 | - [レシピ](#レシピ) 37 | - [差分納品](#差分納品) 38 | 39 | ## 推奨環境 40 | 41 | - Mac OS X または Windows 42 | - Yarn 43 | - Node.js 8.9.0 以降 44 | - EditorConfig および Prettier をサポートするエディタ 45 | 46 | ## インストール 47 | 48 | ```bash 49 | git clone https://github.com/yuheiy/real-world-website-boilerplate.git my-web 50 | cd my-web 51 | rm -rf .git 52 | yarn install 53 | ``` 54 | 55 | このプロジェクトの Git リポジトリをクローンした上で、Node.js の依存モジュールをインストールしてください。 56 | 57 | ## 開発用サーバー 58 | 59 | ```bash 60 | yarn start 61 | ``` 62 | 63 | ソースファイルの監視を開始して、開発用サーバーを起動します。`--prod` フラグを指定するとビルドが本番用になります。 64 | 65 | ## 本番用ビルド 66 | 67 | ```bash 68 | yarn build 69 | ``` 70 | 71 | [`dist/`](#dist)に本番用にビルドされたファイルが出力されます。 72 | 73 | ## ディレクトリ構成 74 | 75 | ``` 76 | . 77 | ├── .tmp/ 78 | │   ├── assets/ 79 | │   │   ├── main.bundle.css 80 | │   │   ├── main.bundle.css.map 81 | │   │   ├── main.module.bundle.js 82 | │   │   ├── main.module.bundle.js.map 83 | │   │   ├── main.nomodule.bundle.js 84 | │   │   └── main.nomodule.bundle.js.map 85 | │   └── index.html 86 | ├── dist/ 87 | │   ├── assets/ 88 | │   │   ├── logo.svg 89 | │   │   ├── main.bundle.css 90 | │   │   ├── main.module.bundle.js 91 | │   │   ├── main.nomodule.bundle.js 92 | │   │   └── ogp.png 93 | │   ├── favicon.ico 94 | │   └── index.html 95 | ├── public/ 96 | │   ├── assets/ 97 | │   │   ├── logo.svg 98 | │   │   └── ogp.png 99 | │   └── favicon.ico 100 | ├── src/ 101 | │   ├── css/ 102 | │   │   └── main.scss 103 | │   ├── html/ 104 | │   │   ├── _data/ 105 | │   │   │   └── meta.yml 106 | │   │   ├── index.pug 107 | │   │   └── index.yml 108 | │   └── js/ 109 | │   ├── main.js 110 | │   ├── polyfill.common.js 111 | │   ├── polyfill.module.js 112 | │   └── polyfill.nomodule.js 113 | ├── vendor-public/ 114 | │   └── common.css 115 | ├── gulpfile.js 116 | └── package.json 117 | ``` 118 | 119 | ### `src/` 120 | 121 | コンパイルなどの処理をさせるファイルはこのディレクトリに配置します。 122 | 123 | ### `src/html/` 124 | 125 | HTML にコンパイルされる前のテンプレートファイルと、テンプレートから参照するためのデータファイルをこのディレクトリに配置します。配置されたファイルは、同じ階層を維持したままプロジェクトのルートディレクトリに対応付けされます。例えば`src/html/product/drink.pug`は`/product/drink.html`として出力されます。 126 | 127 | [サブディレクトリが指定](#サブディレクトリ)されていれば、サブディレクトリ以下の階層に対応付けされます。 128 | 129 | 詳細は[HTML テンプレート](#html-テンプレート)を参照してください。 130 | 131 | ### `src/css/` 132 | 133 | CSS にコンパイルされる前の Sass ファイルをこのディレクトリに配置します。`src/css/main.scss`をエントリーポイントとして、[アセットディレクトリ](#アセットディレクトリ)直下に`main.bundle.css`として出力されます。 134 | 135 | ### `src/js/` 136 | 137 | コンパイルされる前の JavaScript ファイルをこのディレクトリに配置します。`src/js/main.js`をエントリーポイントとして、[アセットディレクトリ](#アセットディレクトリ)直下に`main.bundle.js`として出力されます。 138 | 139 | ファイル内では次のグローバル変数を参照できます。 140 | 141 | `__DEV__`は、開発時は`true`になり、本番用ビルドでは`false`になります。 142 | 143 | `__BASE_PATH__`は、デフォルトでは`/`になり、[サブディレクトリが指定](#サブディレクトリ)されていれば`/path/to/subdir/`のようになります。 144 | 145 | ### `public/` 146 | 147 | 静的ファイルをこのディレクトリに配置します。各ファイルはプロジェクトのルートディレクトリに対応付けされます。`public/favicon.ico`は`/favicon.ico`として出力されます。 148 | 149 | [サブディレクトリが指定](#サブディレクトリ)されていれば、サブディレクトリ以下の階層に対応付けされます。 150 | 151 | ### `vendor-public/` 152 | 153 | デプロイに含めないファイルをこのディレクトリに配置します。各ファイルはプロジェクトのルートディレクトリに対応付けされます。`vendor-public/common.css`は`/common.css`として出力されます。 154 | 155 | このディレクトリ内のファイルは本番用ビルドの実行時に[`dist/`](#dist)に出力されません。開発時には必要であっても、デプロイに含める必要はないファイルを配置できます。 156 | 157 | [サブディレクトリが指定](#サブディレクトリ)されていても対応付けされるパスに*影響しません*。 158 | 159 | ### `dist/` 160 | 161 | 本番用ビルドの実行時にはこのディレクトリにファイルが出力されます。 162 | 163 | [サブディレクトリが指定](#サブディレクトリ)されていれば、このディレクトリを基準としたサブディレクトリにファイルが出力されます。 164 | 165 | **このディレクトリ以下のファイルを直接編集することは推奨されません。** 166 | 167 | ### `.tmp/` 168 | 169 | 開発用サーバーの起動中に一時的に必要になるファイルがこのディレクトリに出力されます。 170 | 171 | **このディレクトリ以下のファイルを直接編集することは推奨されません。** 172 | 173 | ## HTML テンプレート 174 | 175 | [`src/html/`](#srchtml)に配置されたファイルは、同じ階層を維持したままプロジェクトのルートディレクトリに対応付けされます。例えば`src/html/foo/bar.pug`は`/foo/bar.html`として出力されます。 176 | 177 | ただし、`_`から始まるファイルおよびディレクトリは無視されます。 178 | 179 | また、開発用サーバー起動中はリクエストされたファイルのみをコンパイルします。それによってファイル数の増大が開発中のビルド時間に影響しないようになっています。 180 | 181 | ### データファイル 182 | 183 | テンプレート内では変数としてデータファイルの内容を参照できます。データファイルの種類には 2 つあり、単一ページのみに適用されるデータファイルと、全てのページに適用されるデータファイルがあります。規約に沿ったファイル名でファイルを作成することでデータを設定できます([`data`](#data)と[`page`](#page)を参照してください)。 184 | 185 | データファイルの形式としては、JavaScript、YAML、JSON をサポートしています。 186 | 187 | 形式が JavaScript であれば、テンプレート内で使用する値かその値を返す関数をエクスポートしてください。関数が Promise を返す場合は、解決後の結果がデータとして読み込まれます。 188 | 189 | また関数をエクスポートした場合には`absPath`関数などのテンプレート用のオブジェクトが渡されます。詳しくは`config/siteGenerator/task.js`の`defaultLocals`変数を参照してください。 190 | 191 | ```js 192 | // ok 193 | module.exports = { 194 | title: 'Document title', 195 | post: { 196 | title: 'Post title', 197 | }, 198 | } 199 | 200 | // ok 201 | module.exports = (_locals) => { 202 | return { 203 | title: 'Document title', 204 | post: { 205 | title: 'Post title', 206 | }, 207 | } 208 | } 209 | 210 | // ok 211 | module.exports = async (_locals) => { 212 | const post = await fetchPost() 213 | return { 214 | title: 'Document title', 215 | post, 216 | } 217 | } 218 | ``` 219 | 220 | 内容はいずれのファイル形式であってもキャッシュされず、ファイルをコンパイルするたびに毎回読み込み直されます。 221 | 222 | 拡張子が`.js`、`.yml`、`.yaml`、`.json`の順に優先されます。 223 | 224 | ### 変数 225 | 226 | テンプレートファイルではいくつかの変数を参照できます。 227 | 228 | #### `data` 229 | 230 | `src/html/_data/`直下のデータファイルは`data`変数に対応付けされます。`src/html/_data/meta.json`というデータファイルは、`data.meta`からその内容を参照できます。 231 | 232 | #### `page` 233 | 234 | テンプレートファイルと同じ名前のデータファイルを参照できます。テンプレートファイルが`src/html/page.pug`であれば、データファイルは`src/html/page.json`になります。 235 | 236 | #### `page.path` 237 | 238 | テンプレートファイル自身の出力先のパスを参照できます。`src/html/page.pug`は`/page.html`になります。また、`index.html`は省略可能なファイル名であるため、`src/html/index.pug`は`/`になります。 239 | 240 | [サブディレクトリが指定](#サブディレクトリ)されていてもこの値に影響はありません。サブディレクトリを含めた絶対パスは`absPath(page.path)`で取得できます。 241 | 242 | データファイルに`path`が指定されていればこの値で上書きされます。 243 | 244 | #### `absPath(filePath)` 245 | 246 | `filePath`をプロジェクトのルートディレクトリを基準とした絶対パスに変換します。 247 | 248 | [サブディレクトリが指定](#サブディレクトリ)されていれば、`absPath('page.html')`は`/path/to/subdir/page.html`のようになります。 249 | 250 | #### `assetPath(filePath)` 251 | 252 | `filePath`を[アセットディレクトリ](#アセットディレクトリ)を基準とした絶対パスに変換します。`assetPath('ogp.png')`は`/assets/ogp.png`になります。 253 | 254 | [サブディレクトリが指定](#サブディレクトリ)されていれば、`assetPath('ogp.png')`は`/path/to/subdir/assets/page.html`のようになります。 255 | 256 | #### `__DEV__` 257 | 258 | 本番用ビルドが有効になっているかを判断できるフラグです。開発用サーバーが`--prod`フラグ無しで起動中のときには`false`になります。開発用サーバーを`--prod`フラグ付きで起動したときか、本番用ビルドを実行したときには`true`になります。 259 | 260 | ## 設定 261 | 262 | ### サブディレクトリ 263 | 264 | プロジェクトがデプロイされる URL の基準になるディレクトリを指定できます。ルートディレクトリにデプロイされるのであれば指定の必要はありません。 265 | 266 | npm-scripts の`start`コマンドおよび`build`コマンドに、`--subdir path/to/subdir`というように引数を追加することで指定できます。 267 | 268 | ```diff 269 | "scripts": { 270 | - "start": "gulp", 271 | + "start": "gulp --subdir path/to/subdir", 272 | - "build": "gulp build --prod" 273 | + "build": "gulp build --prod --subdir path/to/subdir" 274 | }, 275 | ``` 276 | 277 | ### アセットディレクトリ 278 | 279 | コンパイル後の CSS ファイルや JavaScript ファイルが出力されるディレクトリを変更できます。デフォルトでは`/assets/`です。 280 | 281 | `config/path.js`の`assetsPath`変数を次のように編集すると変更できます。 282 | 283 | ```js 284 | // デフォルト 285 | // `/assets/` 286 | // `/path/to/subdir/assets/` 287 | const assetsPath = path.join(basePath, 'assets') 288 | 289 | // `/` 290 | // `/path/to/subdir/` 291 | const assetsPath = basePath 292 | 293 | // `/assets/` 294 | // `/assets/path/to/subdir/` 295 | const assetsPath = path.join('assets', basePath) 296 | ``` 297 | 298 | ### 独自フラグ 299 | 300 | `config/flag.js`を編集してビルド用のフラグを追加できます。 301 | 302 | ```js 303 | const isDev = !process.argv.includes('--prod') 304 | const isStaging = process.argv.includes('--staging') 305 | 306 | module.exports = { 307 | isDev, 308 | isStaging, 309 | } 310 | ``` 311 | 312 | 必要に応じて`config/siteGenerator/task.js`や`config/webpack.config.js`へ変数を渡すことで参照できるようになります。 313 | 314 | ### Server Side Includes 315 | 316 | このプロジェクトで採用している Browsersync では Server Side Includes を有効にできます。`gulp-tasks/serve.js`を編集して、オプションの`rewriteRules`を次のように実装することで有効にできます。読み込む対象のファイルを`vendor-public/`に配置する場合の実装例です。 317 | 318 | ```js 319 | const fs = require('fs') 320 | 321 | const serve = (done) => { 322 | bs.init( 323 | { 324 | // ... 325 | rewriteRules: [ 326 | { 327 | match: //g, 328 | fn(_req, _res, _match, filePath) { 329 | const srcFilePath = join(vendorPublicDir, filePath) 330 | 331 | if ( 332 | fs.existsSync(srcFilePath) && 333 | fs.statSync(srcFilePath).isFile() 334 | ) { 335 | return fs.readFileSync(srcFilePath, 'utf8') 336 | } else { 337 | return `\`${srcFilePath}\` could not be found` 338 | } 339 | }, 340 | }, 341 | ], 342 | // ... 343 | }, 344 | done, 345 | ) 346 | } 347 | ``` 348 | 349 | これにより次のようなディレクティブが有効になります。 350 | 351 | 352 | ```html 353 | 354 | 355 |
356 | ... 357 |
358 | 359 | 360 | ``` 361 | 362 | ### テンプレートエンジン 363 | 364 | このプロジェクトでは HTML テンプレートのためのテンプレートエンジンに Pug を採用していますが、別のテンプレートエンジンに置き換えることもできます。 365 | 366 | `config/siteGenerator/task.js`の`task`関数を変更して必要に応じたテンプレートエンジンに変更してください。 367 | 368 | また`config/siteGenerator/index.js`の`inputExt`に拡張子が指定されています。使用するテンプレートエンジンに合わせた指定に変更してください。 369 | 370 | ### HTML の整形 371 | 372 | テンプレートエンジンとして Pug を使用している場合、HTML ファイルは圧縮された状態で出力されます。出力される HTML ファイルを整形したければ、[JS Beautifier](https://github.com/beautify-web/js-beautify)などのフォーマッターを使用してください。 373 | 374 | [gulp-jsbeautifier](https://github.com/tarunc/gulp-jsbeautifier)などのプラグインを`config/siteGenerator/task.js`の`task`関数の末尾に追加することでフォーマットできるようになります。 375 | 376 | ```js 377 | const beautify = require('gulp-jsbeautifier') 378 | 379 | const beautifyOpts = { 380 | indent_size: 2, 381 | } 382 | 383 | const task = (stream, handleError) => { 384 | return stream 385 | .pipe(data(readLocals)) 386 | .on('error', handleError) 387 | .pipe(pug(pugOpts)) 388 | .on('error', handleError) 389 | .pipe(beautify(beautifyOpts)) 390 | .on('error', handleError) 391 | } 392 | ``` 393 | 394 | ## レシピ 395 | 396 | ### 差分納品 397 | 398 | 納品ファイルの差分を管理する必要があれば、[`dist/`](#dist)を Git にコミットするようにします。`.gitignore`を次のように変更してください。 399 | 400 | ```diff 401 | # project build output 402 | -/dist/ 403 | /.tmp/ 404 | ``` 405 | 406 | リリースの毎に本番用ビルドを実行して、出力される`dist/`を Git にコミットします。このコミットに Git のタグを付けておくと参照しやすくなります。 407 | 408 | 前回のリリースと対応するコミットが`release-20180101`の場合、次のコマンドで追加・変更したファイルのみを Zip ファイルとして出力できます。 409 | 410 | ```bash 411 | git archive --format=zip --prefix=htdocs/ HEAD:dist `git diff --diff-filter=ACMR --name-only release-20180101 HEAD | grep "^dist/" | sed -e "s/dist\///"` > htdocs.zip 412 | ``` 413 | 414 | 削除したファイルリストは次のコマンドで出力できます。 415 | 416 | ```bash 417 | git diff release-20180101 --name-only --diff-filter=D | grep "^dist" | sed -e "s/dist\///" > deleted-files.txt 418 | ``` 419 | --------------------------------------------------------------------------------