diff --git a/config.js b/config.js index 52961062..24a4a4d1 100644 --- a/config.js +++ b/config.js @@ -1,5 +1,6 @@ module.exports = { url: 'https://www.accessibility-developer-guide.com', + repoUrl: 'https://github.com/Access4all/adg', title: 'Accessibility Developer Guide', description: '', // TODO: Add twitter: '', // TODO: Add diff --git a/gulp/helpers/git-metadata.js b/gulp/helpers/git-metadata.js new file mode 100644 index 00000000..13122e7f --- /dev/null +++ b/gulp/helpers/git-metadata.js @@ -0,0 +1,103 @@ +const childProcess = require('child_process') +const crypto = require('crypto') + +const excludedCommitIds = [ + 'ac195754a6e64604066dafe2f5ad373c2a949ac4', // May 2, 2018 Absolutise paths + '2c9c894097e3aaa65b4775f96c595b476c9b29b9', // May 16, 2018 JSONs + '6b45656eb96adfcf23c6f47dc3564cf64e84d77f', // May 16, 2018 Fix examples links for GitHub + 'f0de1b4faceb9623b881d993e9df44f3f27fa8f3', // May 31, 2018 Fix relative links + 'f03b52f2b54b1c0d0de23027f574202852d3d21a', // Jun 12, 2018 Cleanup + '077c23bfd14a84ba32faac7984231bfb6bfed089', // Jun 15, 2018 Cleanup + '45a0b144e22e3179466b515a70550a409b4ab42c', // Sep 22, 2021 chore: update changed date + 'f6c1a521625159db489d65f7f98030482e418eab', // Aug 20, 2021 feat: change toc placeholder + '8d2f1cf458bd1769c828cff79743d8878cf71276', // Jun 28, 2021 feat: change ToC insertion to manual mode + 'f84cdc3b77f12dc3170717f6025aeadf4c337bbb', // Jan 18, 2024 feat: replace http in urls with https + 'aac742ff1c64c608985a1be3777da79fa70bf292', // Jun 29, 2023 feat: remove manual changed date from markdown files + '34da02f9b8caf03abc590e28d5bae79f8fc89e08' // Jan 27, 2024 ADG-338 feat: unify card text endings +] + +const excludedCommitIdsSet = new Set( + excludedCommitIds.map(id => id.toLowerCase()) +) +const historyYearsLimit = 5 +const gravatarImageSize = 48 + +const getGravatarUrl = email => { + if (!email) { + return '' + } + + const normalizedEmail = String(email).trim().toLowerCase() + + if (!normalizedEmail) { + return '' + } + + const hash = crypto.createHash('sha256').update(normalizedEmail).digest('hex') + + return `https://gravatar.com/avatar/${hash}?s=${gravatarImageSize}&d=mp` +} + +module.exports = ({ githubRepoUrl }) => { + const changedMetadata = {} + + return filePath => { + if (changedMetadata[filePath]) { + return changedMetadata[filePath] + } + + const historyStdout = childProcess.spawnSync( + 'git', + ['log', '--pretty=format:%H%x1f%ci%x1f%an%x1f%ae%x1f%s%x1e', filePath], + { encoding: 'utf8' } + ).stdout + + const filteredHistoryEntries = historyStdout + .split('\x1e') + .map(item => item.trim()) + .filter(Boolean) + .map(item => { + const [ + commitId = '', + changed = '', + changedBy = '', + changedByEmail = '', + message = '' + ] = item.split('\x1f') + + return { + commitId, + changed, + changedBy, + changedByEmail, + gravatarUrl: getGravatarUrl(changedByEmail), + commitUrl: `${githubRepoUrl}/commit/${commitId}`, + message + } + }) + .filter( + entry => + entry.commitId && + !excludedCommitIdsSet.has(entry.commitId.toLowerCase()) + ) + + const latestEntry = filteredHistoryEntries[0] || null + const cutoffDate = new Date() + cutoffDate.setFullYear(cutoffDate.getFullYear() - historyYearsLimit) + const historyEntries = filteredHistoryEntries.filter(entry => { + const changedDate = new Date(entry.changed) + return !Number.isNaN(changedDate.getTime()) && changedDate >= cutoffDate + }) + + const metadata = { + changed: latestEntry ? latestEntry.changed : '', + changedBy: latestEntry ? latestEntry.changedBy : '', + changedByEmail: latestEntry ? latestEntry.changedByEmail : '', + gravatarUrl: latestEntry ? latestEntry.gravatarUrl : '', + historyEntries + } + + changedMetadata[filePath] = metadata + return metadata + } +} diff --git a/gulp/html.js b/gulp/html.js index cfa15dc9..853b8d0f 100644 --- a/gulp/html.js +++ b/gulp/html.js @@ -1,4 +1,3 @@ -const child_process = require('child_process') const gulp = require('gulp') const handlebars = require('gulp-hb') // const prettify = require('gulp-prettify') @@ -24,6 +23,15 @@ const getUrl = (filePath, base) => { .replace(/\/$/, '') } +const getCurrentUrl = (filePath, base) => { + const relPath = path.relative(base, filePath) + const lastSeparatorIndex = relPath.lastIndexOf(path.sep) + + return ( + lastSeparatorIndex >= 0 ? relPath.substring(0, lastSeparatorIndex) : '' + ).replace(pathSeparatorRegExp, '/') +} + const getLayout = (layoutName, layouts) => { layoutName = layoutName || 'layout' @@ -131,8 +139,7 @@ const flattenNavigation = items => return acc }, []) -// Cache changed dates -const changedDates = {} +const recentUpdatesLimit = 8 module.exports = (config, cb) => { const datetime = importFresh('./helpers/datetime') @@ -140,6 +147,34 @@ module.exports = (config, cb) => { const metatags = importFresh('./helpers/metatags') const Feed = importFresh('./helpers/rss') const appConfig = importFresh('../config') + const githubRepoUrl = appConfig.repoUrl + const getGitMetadata = importFresh('./helpers/git-metadata')({ + githubRepoUrl + }) + const getRecentlyUpdatedPages = currentFilePath => + files + .filter(file => !file.frontMatter.navigation_ignore) + .map(file => { + const metadata = getGitMetadata(file.path) + + return { + title: file.data.title, + lead: file.data.lead, + url: getCurrentUrl(file.path, config.base), + changed: metadata.changed, + changedBy: metadata.changedBy, + gravatarUrl: metadata.gravatarUrl + } + }) + .filter( + page => + page.title && + page.url && + page.changed && + page.url !== getCurrentUrl(currentFilePath, config.base) + ) + .sort((a, b) => new Date(b.changed) - new Date(a.changed)) + .slice(0, recentUpdatesLimit) const files = [] const sitemap = [] @@ -240,9 +275,7 @@ module.exports = (config, cb) => { try { const layout = getLayout(file.frontMatter.layout, layouts) const relPath = path.relative('./pages', file.path) - const currentUrl = relPath - .substring(0, relPath.lastIndexOf(path.sep)) - .replace(pathSeparatorRegExp, '/') + const currentUrl = getCurrentUrl(file.path, config.base) const prevNext = {} const breadcrumb = [] const subPages = [] @@ -261,19 +294,27 @@ module.exports = (config, cb) => { site_name: appConfig.title, url: `${appConfig.url}/${currentUrl}` } - const dateChanged = - changedDates[file.path] || - child_process.spawnSync( - 'git', - ['log', '-1', '--pretty=format:%ci', file.path], - { encoding: 'utf8' } - ).stdout - - changedDates[file.path] = dateChanged + const metadata = getGitMetadata(file.path) file.data = Object.assign({}, file.data, { changed: - dateChanged && dateChanged.length > 0 ? dateChanged : null, + metadata.changed && metadata.changed.length > 0 + ? metadata.changed + : null, + changedBy: + metadata.changedBy && metadata.changedBy.length > 0 + ? metadata.changedBy + : null, + gravatarUrl: + metadata.gravatarUrl && metadata.gravatarUrl.length > 0 + ? metadata.gravatarUrl + : null, + historyEntries: + metadata.historyEntries && metadata.historyEntries.length > 0 + ? metadata.historyEntries + : [], + hasHistoryEntries: + metadata.historyEntries && metadata.historyEntries.length > 0, title: file.data.title, contents: file.contents, navigation: pageNavigation, @@ -287,11 +328,13 @@ module.exports = (config, cb) => { level: 1 })) : subPages, + recentlyUpdatedPages: + currentUrl === '' ? getRecentlyUpdatedPages(file.path) : [], metatags: metatags.generateTags(metatagsData), breadcrumb: breadcrumb.sort((a, b) => { return a.url.length - b.url.length }), - fileHistory: `https://github.com/Access4all/adg/commits/main/pages/${relPath}` + fileHistory: `${githubRepoUrl}/commits/main/pages/${relPath}` }) sitemap.push({ @@ -323,6 +366,20 @@ module.exports = (config, cb) => { }, helpers: { formatDate: datetime.formatDate, + truncateText: function (text, maxLength) { + if (!text) { + return '' + } + + const normalizedText = String(text).trim().replace(/\s+/g, ' ') + const limit = Number(maxLength) + + if (!Number.isFinite(limit) || normalizedText.length <= limit) { + return normalizedText + } + + return `${normalizedText.slice(0, limit).trimEnd()}...` + }, eq: function (v1, v2, options) { if (v1 === v2) { return options.fn(this) diff --git a/src/components/content/author/_author.scss b/src/components/content/author/_author.scss new file mode 100644 index 00000000..a8cf7efd --- /dev/null +++ b/src/components/content/author/_author.scss @@ -0,0 +1,20 @@ +.author { + display: inline-flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + font-size: 16px; +} + +.author__avatar { + display: block; + width: 32px; + height: 32px; + border-radius: 999px; + border: 1px solid rgba(0, 0, 0, 0.08); + flex: 0 0 auto; +} + +.author__name { + min-width: 0; +} diff --git a/src/components/content/author/author.hbs b/src/components/content/author/author.hbs new file mode 100644 index 00000000..68e88682 --- /dev/null +++ b/src/components/content/author/author.hbs @@ -0,0 +1,13 @@ + + {{#if gravatarUrl}} + + {{/if}} + {{name}} + diff --git a/src/components/content/meta-info/_meta-info.scss b/src/components/content/meta-info/_meta-info.scss index 444366c6..d04934ba 100644 --- a/src/components/content/meta-info/_meta-info.scss +++ b/src/components/content/meta-info/_meta-info.scss @@ -1,21 +1,91 @@ .meta-info { @include rem(padding-top, $gutter); + gap: 1em; display: flex; - font-size: 0.8rem; + flex-wrap: wrap; + align-items: center; + font-size: 16px; border-top: 1px solid var(--theme-main-color-inverse, $c-white); .theme-contribution & { border-color: var(--theme-color-medium, $c-gray); } -} -.meta-info__title { - margin-right: 0.5em; + @include mobile-portrait { + align-items: flex-start; + } } .meta-info__logo { display: block; - width: 1.5em; - height: 1.5em; + width: 32px; + height: 32px; +} + +.meta-info__updated { + margin-right: 0; + font-weight: 500; +} + +.meta-info__author { + margin-right: auto; +} + +.meta-info__link { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.meta-info-history { + font-size: 14px; + width: 100%; +} + +.meta-info-history__summary { + list-style: none; + cursor: pointer; +} + +.meta-info-history__summary::-webkit-details-marker { + display: none; +} + +.meta-info-history__summary::before { + content: '▽'; + margin-right: 0.5em; + font-size: 1em; + line-height: 1; +} + +.meta-info-history[open] .meta-info-history__summary::before { + content: '△'; +} + +.meta-info-history__summary:focus-visible { + outline: 2px solid currentcolor; + outline-offset: 2px; +} + +.meta-info-history__list { + margin-top: 0.5em; + font-size: 14x; +} + +.meta-info-history__item { + margin-bottom: 0.5em; +} + +.meta-info-history__date { + font-weight: 600; +} + +.meta-info-history__message { + margin-left: 0.5em; +} + +.meta-info-history__author { + margin-left: 0.5em; + font-style: italic; } diff --git a/src/components/content/meta-info/meta-info.hbs b/src/components/content/meta-info/meta-info.hbs index 2f708131..a21dde80 100644 --- a/src/components/content/meta-info/meta-info.hbs +++ b/src/components/content/meta-info/meta-info.hbs @@ -2,14 +2,38 @@

About this page

-

- - Page history: +

+ + Last updated: {{ formatDate changed }} - - - {{{ inlineSvg "src/assets/img/logo/github.svg" class="meta-info__logo" }}} - GitHub source - - -

+ {{#if changedBy}} + {{> "content/author/author" name=changedBy gravatarUrl=gravatarUrl className="meta-info__author"}} + {{/if}} + + {{{ inlineSvg "src/assets/img/logo/github.svg" class="meta-info__logo" }}} + View full history on GitHub + + {{#if hasHistoryEntries}} +
+ + Show recent relevant commits for this page + + Expands to a list of commit messages, dates, and authors. + + + +
+ {{/if}} +
diff --git a/src/components/content/recent-pages/_recent-pages.scss b/src/components/content/recent-pages/_recent-pages.scss new file mode 100644 index 00000000..7fcc542d --- /dev/null +++ b/src/components/content/recent-pages/_recent-pages.scss @@ -0,0 +1,68 @@ +.recent-pages { + @include rem(margin-top, $gutter); +} + +.recent-pages__list { + margin: 0; + padding: 0; + list-style: none; + display: grid; + gap: 1em; +} + +.recent-pages__item { + margin: 0; +} + +.recent-pages__link { + padding: 1em; + + display: block; + background: var(--theme-main-color-inverse, $c-white); + border: 1px solid var(--theme-color-medium, $c-gray); + border-radius: 10px; + box-shadow: 0 4px 14px 0 rgba(0, 0, 0, 0.08); + color: inherit; + text-decoration: none; + transition: border-color 0.15s ease, box-shadow 0.15s ease, + transform 0.15s ease; + + &:hover, + &:focus { + border-color: var(--theme-color-dark); + box-shadow: 0 8px 24px 0 rgba(0, 0, 0, 0.12); + transform: translateY(-1px); + text-decoration: none; + } +} + +.recent-pages__date { + display: block; + margin-bottom: 4px; + font-size: 16px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.recent-pages__title { + margin: 0; + font-size: 24px; + line-height: 1.25; +} + +.recent-pages__title-link { + color: var(--theme-color-dark); + text-decoration: none; +} + +.recent-pages__lead { + margin-top: 0.75em; + margin-bottom: 1.5em; + font-size: 16px; + line-height: 1.5; +} + +.recent-pages__author { + display: flex; +} diff --git a/src/components/content/recent-pages/recent-pages.hbs b/src/components/content/recent-pages/recent-pages.hbs new file mode 100644 index 00000000..fc78a602 --- /dev/null +++ b/src/components/content/recent-pages/recent-pages.hbs @@ -0,0 +1,24 @@ +
+
+

Recent changes

+
+ + +
diff --git a/src/templates/layout.hbs b/src/templates/layout.hbs index f517a814..468a9445 100644 --- a/src/templates/layout.hbs +++ b/src/templates/layout.hbs @@ -55,6 +55,9 @@ {{#if subPages}} {{> "content/cardmenu/cardmenu" menuitem=subPages}} {{/if}} + {{#if recentlyUpdatedPages}} + {{> "content/recent-pages/recent-pages" pages=recentlyUpdatedPages}} + {{/if}} {{> "content/prev_next/prev_next" prev=previousPage next=nextPage}} {{#notEq section 'welcome'}}