@@ -5,7 +5,7 @@
|
||||
# --------------------------------------------------------------------------------
|
||||
# To update the sha, run `docker pull node:$VERSION-alpine`
|
||||
# look for something like: `Digest: sha256:0123456789abcdef`
|
||||
FROM node:20-alpine@sha256:66c7d989b6dabba6b4305b88f40912679aebd9f387a5b16ffa76dfb9ae90b060 as base
|
||||
FROM node:20-alpine@sha256:66f7f89199daea88a6b5d5aadaa6d20f7a16a90fc35274deda8e901e267d4bd7 as base
|
||||
|
||||
# This directory is owned by the node user
|
||||
ARG APP_HOME=/home/node/app
|
||||
|
||||
@@ -35,6 +35,8 @@ export default async function contextualize(
|
||||
|
||||
const context: Context = {}
|
||||
req.context = context
|
||||
console.log('CREATING CONTEXT')
|
||||
|
||||
req.context.process = { env: {} }
|
||||
|
||||
// define each context property explicitly for code-search friendliness
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { Response, NextFunction } from 'express'
|
||||
|
||||
import type { ExtendedRequest } from '@/types'
|
||||
import { defaultCacheControl } from './cache-control.js'
|
||||
|
||||
export default function fastHead(req, res, next) {
|
||||
export default function fastHead(req: ExtendedRequest, res: Response, next: NextFunction) {
|
||||
if (!req.context) throw new Error('request is not contextualized')
|
||||
const { context } = req
|
||||
const { page } = context
|
||||
if (page) {
|
||||
@@ -5,10 +5,19 @@
|
||||
// solution to serve this directly.
|
||||
import fs from 'fs'
|
||||
|
||||
import type { Response, NextFunction } from 'express'
|
||||
|
||||
import type { ExtendedRequest } from '@/types'
|
||||
import { SURROGATE_ENUMS, setFastlySurrogateKey } from './set-fastly-surrogate-key.js'
|
||||
import { assetCacheControl } from './cache-control.js'
|
||||
|
||||
const MAP = {
|
||||
type IconConfig = {
|
||||
contentType: string
|
||||
buffer: () => Buffer
|
||||
}
|
||||
const MAP: {
|
||||
[uri: string]: IconConfig
|
||||
} = {
|
||||
'/favicon.ico': {
|
||||
contentType: 'image/x-icon',
|
||||
buffer: getBuffer('assets/images/site/favicon.ico'),
|
||||
@@ -35,8 +44,8 @@ MAP['/apple-touch-icon-precomposed.png'] = MAP['/apple-touch-icon.png']
|
||||
MAP['/apple-touch-icon-120x120-precomposed.png'] = MAP['/apple-touch-icon-120x120.png']
|
||||
MAP['/apple-touch-icon-152x152-precomposed.png'] = MAP['/apple-touch-icon-152x152.png']
|
||||
|
||||
function getBuffer(filePath) {
|
||||
let buffer
|
||||
function getBuffer(filePath: string) {
|
||||
let buffer: Buffer
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`${filePath} not found on disk`)
|
||||
}
|
||||
@@ -51,7 +60,7 @@ function getBuffer(filePath) {
|
||||
}
|
||||
}
|
||||
|
||||
export default function favicons(req, res, next) {
|
||||
export default function favicons(req: ExtendedRequest, res: Response, next: NextFunction) {
|
||||
if (!MAP[req.path]) return next()
|
||||
|
||||
// This makes sure the CDN caching survives each production deployment.
|
||||
@@ -49,14 +49,14 @@ import features from '@/versions/middleware/features.js'
|
||||
import productExamples from './context/product-examples'
|
||||
import productGroups from './context/product-groups'
|
||||
import featuredLinks from '@/landings/middleware/featured-links'
|
||||
import learningTrack from '@/learning-track/middleware/learning-track.js'
|
||||
import learningTrack from '@/learning-track/middleware/learning-track'
|
||||
import next from './next.js'
|
||||
import renderPage from './render-page.js'
|
||||
import assetPreprocessing from '@/assets/middleware/asset-preprocessing'
|
||||
import archivedAssetRedirects from '@/archives/middleware/archived-asset-redirects'
|
||||
import favicons from './favicons.js'
|
||||
import favicons from './favicons'
|
||||
import setStaticAssetCaching from '@/assets/middleware/static-asset-caching'
|
||||
import fastHead from './fast-head.js'
|
||||
import fastHead from './fast-head'
|
||||
import fastlyCacheTest from './fastly-cache-test.js'
|
||||
import trailingSlashes from './trailing-slashes.js'
|
||||
import fastlyBehavior from './fastly-behavior.js'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { SURROGATE_ENUMS } from '#src/frame/middleware/set-fastly-surrogate-key.js'
|
||||
import { get } from '#src/tests/helpers/e2etest.js'
|
||||
import { SURROGATE_ENUMS } from '@/frame/middleware/set-fastly-surrogate-key.js'
|
||||
import { get } from '@/tests/helpers/e2etest.js'
|
||||
|
||||
describe('favicon assets', () => {
|
||||
vi.setConfig({ testTimeout: 60 * 1000 })
|
||||
@@ -1,25 +1,48 @@
|
||||
import { getPathWithoutLanguage, getPathWithoutVersion } from '#src/frame/lib/path-utils.js'
|
||||
import getLinkData from '../lib/get-link-data.js'
|
||||
import { renderContent } from '#src/content-render/index.js'
|
||||
import { getDeepDataByLanguage } from '#src/data-directory/lib/get-data.js'
|
||||
import type { Response, NextFunction } from 'express'
|
||||
|
||||
import type {
|
||||
Context,
|
||||
ExtendedRequest,
|
||||
LearningTrack,
|
||||
LearningTracks,
|
||||
TrackGuide,
|
||||
Page,
|
||||
} from '@/types'
|
||||
import { getPathWithoutLanguage, getPathWithoutVersion } from '@/frame/lib/path-utils.js'
|
||||
import getLinkData from '../lib/get-link-data.js'
|
||||
import { renderContent } from '@/content-render/index.js'
|
||||
import { getDeepDataByLanguage } from '@/data-directory/lib/get-data.js'
|
||||
|
||||
export default async function learningTrack(
|
||||
req: ExtendedRequest,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
if (!req.context) throw new Error('request is not contextualized')
|
||||
|
||||
export default async function learningTrack(req, res, next) {
|
||||
const noTrack = () => {
|
||||
req.context.currentLearningTrack = {}
|
||||
req.context!.currentLearningTrack = null
|
||||
return next()
|
||||
}
|
||||
|
||||
if (!req.context.page) return next()
|
||||
|
||||
const trackName = req.query.learn
|
||||
if (!trackName) return noTrack()
|
||||
if (!req.query.learn) return noTrack()
|
||||
if (Array.isArray(req.query.learn)) return noTrack()
|
||||
const trackName = req.query.learn as string
|
||||
|
||||
let trackProduct = req.context.currentProduct
|
||||
const allLearningTracks = getDeepDataByLanguage('learning-tracks', req.language)
|
||||
if (req.langauge !== 'en') {
|
||||
let trackProduct = req.context.currentProduct as string
|
||||
// TODO: Once getDeepDataByLanguage is ported to TS
|
||||
// a more appropriate API would be to use `getDeepDataByLanguage<LearningTracks)(...)`
|
||||
const allLearningTracks = getDeepDataByLanguage('learning-tracks', req.language) as LearningTracks
|
||||
|
||||
if (req.language !== 'en') {
|
||||
// Don't trust the `.guides` from the translation. It too often has
|
||||
// broken Liquid (e.g. `{% ifversion fpt 또는 ghec 또는 ghes %}`)
|
||||
const allEnglishLearningTracks = getDeepDataByLanguage('learning-tracks', 'en')
|
||||
const allEnglishLearningTracks = getDeepDataByLanguage(
|
||||
'learning-tracks',
|
||||
'en',
|
||||
) as LearningTracks
|
||||
for (const [key, tracks] of Object.entries(allLearningTracks)) {
|
||||
if (!(key in allEnglishLearningTracks)) {
|
||||
// This can happen when the translation of
|
||||
@@ -47,7 +70,12 @@ export default async function learningTrack(req, res, next) {
|
||||
// the case where a learning track has guide paths for a different product
|
||||
// than the current learning track product.
|
||||
if (!tracksPerProduct) {
|
||||
trackProduct = req.query.learnProduct
|
||||
if (!req.query.learnProduct) return noTrack()
|
||||
if (Array.isArray(req.query.learnProduct)) {
|
||||
trackProduct = req.query.learnProduct[0] as string
|
||||
} else {
|
||||
trackProduct = req.query.learnProduct as string
|
||||
}
|
||||
tracksPerProduct = allLearningTracks[trackProduct]
|
||||
}
|
||||
if (!tracksPerProduct) return noTrack()
|
||||
@@ -57,14 +85,14 @@ export default async function learningTrack(req, res, next) {
|
||||
|
||||
// The trackTitle comes from a data .yml file and may use Liquid templating, so we need to render it
|
||||
const renderOpts = { textOnly: true }
|
||||
const trackTitle = await renderContent(track.title, req.context, renderOpts)
|
||||
const trackTitle = (await renderContent(track.title, req.context, renderOpts)) as string
|
||||
|
||||
const currentLearningTrack = { trackName, trackProduct, trackTitle }
|
||||
const currentLearningTrack: LearningTrack = { trackName, trackProduct, trackTitle }
|
||||
const guidePath = getPathWithoutLanguage(getPathWithoutVersion(req.pagePath))
|
||||
|
||||
// The raw track.guides will return all guide paths, need to use getLinkData
|
||||
// so we only get guides available in the current version
|
||||
const trackGuides = await getLinkData(track.guides, req.context)
|
||||
const trackGuides = (await getLinkData(track.guides, req.context)) as TrackGuide[]
|
||||
|
||||
const trackGuidePaths = trackGuides.map((guide) => {
|
||||
return getPathWithoutLanguage(getPathWithoutVersion(guide.href))
|
||||
@@ -97,8 +125,13 @@ export default async function learningTrack(req, res, next) {
|
||||
|
||||
if (guideIndex > 0) {
|
||||
const prevGuidePath = trackGuidePaths[guideIndex - 1]
|
||||
const result = await getLinkData(prevGuidePath, req.context, { title: true, intro: false })
|
||||
if (!result) return noTrack()
|
||||
const resultData = await getLinkData(prevGuidePath, req.context, {
|
||||
title: true,
|
||||
intro: false,
|
||||
fullTitle: false,
|
||||
})
|
||||
if (!resultData) return noTrack()
|
||||
const result = resultData as { href: string; page: Page; title: string }
|
||||
|
||||
const href = result.href
|
||||
const title = result.title
|
||||
@@ -107,8 +140,13 @@ export default async function learningTrack(req, res, next) {
|
||||
|
||||
if (guideIndex < trackGuidePaths.length - 1) {
|
||||
const nextGuidePath = trackGuidePaths[guideIndex + 1]
|
||||
const result = await getLinkData(nextGuidePath, req.context, { title: true, intro: false })
|
||||
if (!result) return noTrack()
|
||||
const resultData = await getLinkData(nextGuidePath, req.context, {
|
||||
title: true,
|
||||
intro: false,
|
||||
fullTitle: false,
|
||||
})
|
||||
if (!resultData) return noTrack()
|
||||
const result = resultData as { href: string; page: Page; title: string }
|
||||
|
||||
const href = result.href
|
||||
const title = result.title
|
||||
@@ -123,7 +161,11 @@ export default async function learningTrack(req, res, next) {
|
||||
|
||||
// Find the index of a learning track guide path in an array of guide paths,
|
||||
// return -1 if not found.
|
||||
async function indexOfLearningTrackGuide(trackGuidePaths, guidePath, context) {
|
||||
async function indexOfLearningTrackGuide(
|
||||
trackGuidePaths: string[],
|
||||
guidePath: string,
|
||||
context: Context,
|
||||
) {
|
||||
let guideIndex = -1
|
||||
|
||||
for (let i = 0; i < trackGuidePaths.length; i++) {
|
||||
@@ -1,16 +1,19 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
import { loadPages, loadPageMap } from '#src/frame/lib/page-data.js'
|
||||
import loadRedirects from '#src/redirects/lib/precompile.js'
|
||||
import { getDeepDataByLanguage } from '#src/data-directory/lib/get-data.js'
|
||||
import { checkURL } from '#src/tests/helpers/check-url.js'
|
||||
import type { LearningTracks } from '@/types'
|
||||
import { loadPages, loadPageMap } from '@/frame/lib/page-data.js'
|
||||
import loadRedirects from '@/redirects/lib/precompile.js'
|
||||
import { getDeepDataByLanguage } from '@/data-directory/lib/get-data.js'
|
||||
import { checkURL } from '@/tests/helpers/check-url.js'
|
||||
|
||||
const pageList = await loadPages(undefined, ['en'])
|
||||
const pages = await loadPageMap(pageList)
|
||||
const redirects = await loadRedirects(pageList)
|
||||
|
||||
describe('learning tracks', () => {
|
||||
const allLearningTracks = getDeepDataByLanguage('learning-tracks', 'en')
|
||||
// TODO: Once getDeepDataByLanguage is ported to TS
|
||||
// a more appropriate API would be to use `getDeepDataByLanguage<LearningTracks)(...)`
|
||||
const allLearningTracks = getDeepDataByLanguage('learning-tracks', 'en') as LearningTracks
|
||||
const topLevels = Object.keys(allLearningTracks)
|
||||
|
||||
test.each(topLevels)('learning-track in data/learning-tracks/%s.yml', (topLevel) => {
|
||||
@@ -33,6 +36,12 @@ describe('learning tracks', () => {
|
||||
expect(length, errorMessage).toEqual(size)
|
||||
}
|
||||
|
||||
type Trouble = {
|
||||
uri: string
|
||||
index: number
|
||||
redirects: string | undefined
|
||||
}
|
||||
type TroubleTuple = [string, Trouble[]]
|
||||
const troubles = Object.entries(learningTracks)
|
||||
.map(([learningTrackKey, learningTrack]) => {
|
||||
return [
|
||||
@@ -42,7 +51,7 @@ describe('learning tracks', () => {
|
||||
.filter(Boolean),
|
||||
]
|
||||
})
|
||||
.filter(([, trouble]) => trouble.length > 0)
|
||||
.filter(([, trouble]) => trouble.length > 0) as TroubleTuple[]
|
||||
|
||||
let errorMessage = `In data/learning-tracks/${topLevel}.yml there are ${troubles.length} guides that are not correct.\n`
|
||||
let fixables = 0
|
||||
34
src/types.ts
34
src/types.ts
@@ -159,6 +159,39 @@ export type Context = {
|
||||
productUserExamples?: ProductExample[]
|
||||
productGroups?: ProductGroup[]
|
||||
featuredLinks?: FeaturedLinksExpanded
|
||||
currentLearningTrack?: LearningTrack | null
|
||||
}
|
||||
export type LearningTracks = {
|
||||
[group: string]: {
|
||||
[track: string]: {
|
||||
title: string
|
||||
description: string
|
||||
versions?: FrontmatterVersions
|
||||
guides: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
export type LearningTrack = {
|
||||
trackName: string
|
||||
trackProduct: string
|
||||
trackTitle: string
|
||||
numberOfGuides?: number
|
||||
currentGuideIndex?: number
|
||||
nextGuide?: {
|
||||
href: string
|
||||
title: string
|
||||
}
|
||||
prevGuide?: {
|
||||
href: string
|
||||
title: string
|
||||
}
|
||||
}
|
||||
|
||||
export type TrackGuide = {
|
||||
href: string
|
||||
page: Page
|
||||
title: string
|
||||
intro: string
|
||||
}
|
||||
|
||||
export type FeaturedLinkExpanded = {
|
||||
@@ -311,6 +344,7 @@ export type Page = {
|
||||
earlyAccessToc?: boolean
|
||||
autogenerated?: string
|
||||
featuredLinks?: FeaturedLinksExpanded
|
||||
redirect_from?: string[]
|
||||
}
|
||||
|
||||
type ChangeLog = {
|
||||
|
||||
Reference in New Issue
Block a user