Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions build/screenshot-examples.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/**
* Screenshot Bootstrap examples using Playwright.
*
* Starts the Astro dev server automatically, waits for it to be ready,
* takes light + dark screenshots at 1x and 2x, then shuts the server down.
*
* Usage:
* node build/screenshot-examples.mjs [--only album,pricing]
*
* Prerequisites:
* npm install -D playwright
* npx playwright install chromium
*
* The script reads examples.yml and saves to:
* site/static/docs/[version]/assets/img/examples/{slug}.png (480×300)
* site/static/docs/[version]/assets/img/examples/{slug}@2x.png (960×600)
* site/static/docs/[version]/assets/img/examples/{slug}-dark.png (480×300)
* site/static/docs/[version]/assets/img/examples/{slug}-dark@2x.png (960×600)
*/

import { readFileSync, mkdirSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { spawn } from 'node:child_process'
import { parse as parseYaml } from 'yaml'
import { chromium } from 'playwright'
import sharp from 'sharp'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const ROOT = path.resolve(__dirname, '..')

// ─── Config ──────────────────────────────────────────────────────────────────

const args = process.argv.slice(2)
const getArg = flag => {
const idx = args.indexOf(flag)
return idx === -1 ? null : args[idx + 1]
}

const ONLY = getArg('--only')?.split(',').map(s => s.trim().toLowerCase()) ?? null

// Astro dev server port (matches astro-dev in package.json)
const PORT = 9001
const BASE_URL = `http://localhost:${PORT}`
const SERVER_TIMEOUT_MS = 60_000
const SERVER_POLL_INTERVAL_MS = 500

// Read docs version from config.yml
const configYml = readFileSync(path.resolve(ROOT, 'config.yml'), 'utf8')
const DOCS_VERSION = parseYaml(configYml).docs_version ?? '6.0'

// Output directory — [version] is a literal Astro dynamic-route folder name
const OUT_DIR = path.resolve(ROOT, 'site/static/docs/[version]/assets/img/examples')
mkdirSync(OUT_DIR, { recursive: true })

// Full-width capture viewport; images are then resized down to thumbnail sizes
const CAPTURE_VIEWPORT = { width: 1440, height: 900 }
// 1x thumbnail: 480×300 | 2x thumbnail: 960×600
const THUMB = { w: 480, h: 300 }

// ─── Dev server ──────────────────────────────────────────────────────────────

/** Spawn the Astro dev server and return the child process. */
function startDevServer() {
console.log('Starting Astro dev server…')
const server = spawn('node', ['node_modules/.bin/astro', 'dev', '--root', 'site', '--port', String(PORT)], {
cwd: ROOT,
stdio: ['ignore', 'pipe', 'pipe']
})
server.stdout.on('data', d => process.stdout.write(`[astro] ${d}`))
server.stderr.on('data', d => process.stderr.write(`[astro] ${d}`))
return server
}

/** Poll until the server responds or timeout is reached. */
async function waitForServer() {
const deadline = Date.now() + SERVER_TIMEOUT_MS

const poll = async () => {
if (Date.now() >= deadline) {
throw new Error(`Dev server did not start within ${SERVER_TIMEOUT_MS / 1000}s`)
}

try {
const res = await fetch(`${BASE_URL}/`)
if (res.ok || res.status < 500) {
console.log('Dev server is ready.\n')
return
}
} catch {
// not up yet
}

await new Promise(resolvePromise => {
setTimeout(resolvePromise, SERVER_POLL_INTERVAL_MS)
})

await poll()
}

await poll()
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

/** Replicate the getSlug() logic used in the Astro components */
function getSlug(name) {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
}

/** Collect all non-external examples from examples.yml */
function getExamples() {
const yml = readFileSync(path.resolve(ROOT, 'site/data/examples.yml'), 'utf8')
const categories = parseYaml(yml)
const result = []
for (const { examples, external } of categories) {
if (external) {
continue
}

for (const example of examples ?? []) {
result.push(example.name)
}
}

return result
}

// ─── Screenshot ──────────────────────────────────────────────────────────────

/**
* Capture the page at full viewport, then resize to the target thumbnail size.
* colorScheme: 'light' | 'dark'
* scale: 1 (480×300) | 2 (960×600)
*/
async function screenshot(page, slug, colorScheme, scale) {
const darkSuffix = colorScheme === 'dark' ? '-dark' : ''
const scaleSuffix = scale === 2 ? '@2x' : ''
const outFile = path.resolve(OUT_DIR, `${slug}${darkSuffix}${scaleSuffix}.png`)

await page.emulateMedia({ colorScheme })
const rawBuffer = await page.screenshot({ type: 'png' })

await sharp(rawBuffer)
.resize(THUMB.w * scale, THUMB.h * scale, { fit: 'cover', position: 'top' })
.toFile(outFile)

console.log(` saved ${outFile.replace(`${ROOT}/`, '')}`)
}

async function run() {
const examples = getExamples()
const filtered = ONLY ? examples.filter(n => ONLY.includes(n.toLowerCase())) : examples

if (filtered.length === 0) {
throw new Error('No examples matched. Check --only values against examples.yml.')
}

const server = startDevServer()

// Ensure the server is killed even if we crash
const cleanup = () => server.kill()
process.on('exit', cleanup)

try {
await waitForServer()

console.log(`Taking screenshots of ${filtered.length} example(s)`)
console.log(`Output → ${OUT_DIR}\n`)

const browser = await chromium.launch()

await Promise.all(filtered.map(async name => {
const slug = getSlug(name)
const url = `${BASE_URL}/docs/${DOCS_VERSION}/examples/${slug}/`
console.log(`→ ${name} (${slug})`)

// Single page load — reuse for light & dark, both scales (sharp handles resizing)
const page = await browser.newPage({ viewport: CAPTURE_VIEWPORT, deviceScaleFactor: 1 })
await page.goto(url, { waitUntil: 'networkidle' })
await page.addStyleTag({ content: '.bd-mode-toggle { display: none !important; }' })
await screenshot(page, slug, 'light', 1)
await screenshot(page, slug, 'light', 2)
await screenshot(page, slug, 'dark', 1)
await screenshot(page, slug, 'dark', 2)
await page.close()
}))

await browser.close()
console.log('\nDone.')
} finally {
server.kill()
}
}

run().catch(error => {
console.error(error)
process.exitCode = 1
})
53 changes: 50 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"docs-prettier-format": "prettier --config site/.prettierrc.json --write --cache site",
"docs-serve": "npm run astro-dev -- --host",
"docs-serve-only": "npx sirv-cli _site --port 9001",
"screenshots": "rm -f site/static/docs/[version]/assets/img/examples/*.png && node build/screenshot-examples.mjs",
"lockfile-lint": "lockfile-lint --allowed-hosts npm --allowed-schemes https: --empty-hostname false --type npm --path package-lock.json",
"update-deps": "ncu -u",
"release": "npm-run-all dist release-sri docs-build release-zip*",
Expand Down Expand Up @@ -179,6 +180,7 @@
"mime": "^4.1.0",
"nodemon": "^3.1.14",
"npm-run-all2": "^8.0.4",
"playwright": "^1.59.1",
"postcss": "^8.5.10",
"postcss-cli": "^11.0.1",
"prettier": "^3.8.3",
Expand All @@ -190,12 +192,14 @@
"rollup-plugin-istanbul": "^5.0.0",
"sass": "^1.99.0",
"sass-true": "^10.1.0",
"sharp": "^0.34.5",
"shelljs": "^0.10.0",
"stylelint": "16.26.1",
"stylelint-config-twbs-bootstrap": "^16.1.0",
"stylelint-order": "^8.1.1",
"terser": "^5.46.1",
"unist-util-visit": "^5.1.0",
"yaml": "^2.8.3",
"zod": "^4.3.6"
},
"files": [
Expand Down
37 changes: 36 additions & 1 deletion site/src/layouts/partials/ExamplesMain.astro
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,15 @@ import { getSlug } from '@libs/utils'
href={`/docs/${getConfig().docs_version}/examples/${getSlug(example.name)}/`}
>
<img
class="img-thumbnail mb-3"
class="img-thumbnail mb-3 examples-thumb"
data-light-src={getVersionedDocsPath(`/assets/img/examples/${getSlug(example.name)}.png`)}
data-light-srcset={`${getVersionedDocsPath(
`/assets/img/examples/${getSlug(example.name)}.png`
)}, ${getVersionedDocsPath(`/assets/img/examples/${getSlug(example.name)}@2x.png`)} 2x`}
data-dark-src={getVersionedDocsPath(`/assets/img/examples/${getSlug(example.name)}-dark.png`)}
data-dark-srcset={`${getVersionedDocsPath(
`/assets/img/examples/${getSlug(example.name)}-dark.png`
)}, ${getVersionedDocsPath(`/assets/img/examples/${getSlug(example.name)}-dark@2x.png`)} 2x`}
srcset={`${getVersionedDocsPath(
`/assets/img/examples/${getSlug(example.name)}.png`
)}, ${getVersionedDocsPath(`/assets/img/examples/${getSlug(example.name)}@2x.png`)} 2x`}
Expand All @@ -81,3 +89,30 @@ import { getSlug } from '@libs/utils'
)
})
}

<script is:inline>
const applyExamplesThumbTheme = () => {
const isDark = document.documentElement.getAttribute('data-bs-theme') === 'dark'
const thumbs = document.querySelectorAll('.examples-thumb')

thumbs.forEach((thumb) => {
const src = isDark ? thumb.dataset.darkSrc : thumb.dataset.lightSrc
const srcset = isDark ? thumb.dataset.darkSrcset : thumb.dataset.lightSrcset

if (src && thumb.getAttribute('src') !== src) {
thumb.setAttribute('src', src)
}

if (srcset && thumb.getAttribute('srcset') !== srcset) {
thumb.setAttribute('srcset', srcset)
}
})
}

applyExamplesThumbTheme()

new MutationObserver(applyExamplesThumbTheme).observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-bs-theme']
})
</script>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/static/docs/[version]/assets/img/examples/album.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/static/docs/[version]/assets/img/examples/album@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/static/docs/[version]/assets/img/examples/badges.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/static/docs/[version]/assets/img/examples/badges@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Comment thread
julien-deramond marked this conversation as resolved.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/static/docs/[version]/assets/img/examples/blog.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/static/docs/[version]/assets/img/examples/blog@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/static/docs/[version]/assets/img/examples/breadcrumbs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/static/docs/[version]/assets/img/examples/buttons.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/static/docs/[version]/assets/img/examples/buttons@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/static/docs/[version]/assets/img/examples/carousel@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/static/docs/[version]/assets/img/examples/cheatsheet.png
Binary file modified site/static/docs/[version]/assets/img/examples/cheatsheet@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/checkout.png
Binary file modified site/static/docs/[version]/assets/img/examples/checkout@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/cover.png
Binary file modified site/static/docs/[version]/assets/img/examples/cover@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/dashboard.png
Binary file modified site/static/docs/[version]/assets/img/examples/dashboard@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/dialogs.png
Binary file modified site/static/docs/[version]/assets/img/examples/dialogs@2x.png
Diff not rendered.
Diff not rendered.
Binary file modified site/static/docs/[version]/assets/img/examples/features.png
Binary file modified site/static/docs/[version]/assets/img/examples/features@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/footers.png
Binary file modified site/static/docs/[version]/assets/img/examples/footers@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/grid.png
Binary file modified site/static/docs/[version]/assets/img/examples/grid@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/headers.png
Binary file modified site/static/docs/[version]/assets/img/examples/headers@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/heroes.png
Binary file modified site/static/docs/[version]/assets/img/examples/heroes@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/jumbotron.png
Binary file modified site/static/docs/[version]/assets/img/examples/jumbotron@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/jumbotrons.png
Binary file modified site/static/docs/[version]/assets/img/examples/jumbotrons@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/list-groups.png
Binary file modified site/static/docs/[version]/assets/img/examples/masonry.png
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only issue I've spotted but this is the behavior I can observe when I load https://deploy-preview-42329--twbs-bootstrap.netlify.app/docs/6.0/examples/masonry/ when 1070x1070 and don't move anything. I don't think this is particularly an issue coming from Playwright. Maybe we can add a movement or something for this specific example. But if we plan to get rid of this example, nevermind, nothing to do.

Binary file modified site/static/docs/[version]/assets/img/examples/masonry@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/navbar-bottom.png
Binary file modified site/static/docs/[version]/assets/img/examples/navbar-fixed.png
Binary file modified site/static/docs/[version]/assets/img/examples/navbars.png
Binary file modified site/static/docs/[version]/assets/img/examples/navbars@2x.png
Diff not rendered.
Diff not rendered.
Binary file modified site/static/docs/[version]/assets/img/examples/pricing.png
Binary file modified site/static/docs/[version]/assets/img/examples/pricing@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/product.png
Binary file modified site/static/docs/[version]/assets/img/examples/product@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/sidebars.png
Binary file modified site/static/docs/[version]/assets/img/examples/sidebars@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/sign-in.png
Binary file modified site/static/docs/[version]/assets/img/examples/sign-in@2x.png
Binary file modified site/static/docs/[version]/assets/img/examples/sticky-footer.png