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
5 changes: 5 additions & 0 deletions .changeset/bright-device-directory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@devicekit/frame': patch
---

Add a crawlable device-profile directory and static profile details to the demo.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:

- run: pnpm format:check

- run: pnpm build
- run: pnpm run check:device-pages

- run: pnpm lint

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Device profiles and a Web Component for rendering phone and tablet previews.

[Open the live demo](https://echotechfe.github.io/devicekit/)

[Browse every device profile page](docs/device-pages.md)

## Install

Install the frame package to render previews:
Expand Down
2 changes: 2 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

[打开在线演示](https://echotechfe.github.io/devicekit/)

[查看全部机型页面](docs/device-pages.md)

## 安装

需要渲染设备外观时安装 frame 包:
Expand Down
183 changes: 183 additions & 0 deletions docs/device-pages.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"format:check": "oxfmt --check packages/devices/src/presets/ios.ts packages/devices/src/presets/android.ts packages/devices/src/presets/harmony.ts",
"test": "pnpm -r test && pnpm run test:scripts",
"test:scripts": "node --test scripts/*.test.mjs",
"generate:device-pages": "pnpm run build && node scripts/generate-device-pages.mjs",
"check:device-pages": "pnpm run build && node scripts/generate-device-pages.mjs --check",
"test:coverage": "pnpm -r test:coverage",
"lint": "oxlint .",
"verify:pack": "pnpm -r build && node scripts/verify-pack.mjs",
Expand Down
12 changes: 12 additions & 0 deletions packages/frame/demo/src/components/DemoPage.astro
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
---
import type { PresetDeviceProfile } from '@devicekit/devices'
import { DEMO_DEVICE_DIRECTORY_URL } from '../../../src/demo-device-seo.js'
import { demoPageMetadata } from '../../../src/demo-page-metadata.js'
import '../../style.css'

interface Props { device?: PresetDeviceProfile }

const { device } = Astro.props
const metadata = demoPageMetadata(device)
const deviceDirectoryHref = new URL(DEMO_DEVICE_DIRECTORY_URL).pathname
---

<!doctype html>
Expand All @@ -25,6 +27,16 @@ const metadata = demoPageMetadata(device)
<div class="panel__eyebrow">devicekit / frame</div>
<div class="panel__header"><h1 id="demo-title">{metadata.heading}</h1><a class="github-link" href="https://github.com/EchoTechFE/devicekit">GitHub ↗</a></div>
<p class="demo-intro" id="demo-intro">{metadata.intro}</p>
<a class="device-directory-link" id="device-directory" href={deviceDirectoryHref}>Browse all device profiles</a>
{device !== undefined && <section class="device-profile" aria-labelledby="device-profile-title">
<h2 id="device-profile-title">Device profile specifications</h2>
<dl>
<div><dt>Operating system</dt><dd>{device.os}</dd></div>
<div><dt>CSS viewport</dt><dd>{device.screen.width} × {device.screen.height}</dd></div>
<div><dt>Pixel ratio</dt><dd>{device.pixelRatio}×</dd></div>
<div><dt>Release year</dt><dd>{device.releaseYear}</dd></div>
</dl>
</section>}

<section class="control-section"><div class="control-section__title">Device</div>
<label class="field"><span>Profile</span><select id="device"></select></label>
Expand Down
42 changes: 42 additions & 0 deletions packages/frame/demo/src/pages/devices/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
import { DEVICES } from '@devicekit/devices'
import { DEMO_DEVICE_DIRECTORY_URL, DEMO_SITE_URL } from '../../../../src/demo-device-seo.js'
import { devicePath } from '../../../../src/demo-device-url.js'
import '../../../style.css'

const deviceHref = (name: string) => new URL(devicePath(name), DEMO_SITE_URL).pathname
---

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>All device profiles | devicekit</title>
<meta name="description" content={`Browse all ${DEVICES.length} iOS, Android and HarmonyOS device profiles available in the devicekit device-frame simulator.`}>
<link rel="canonical" href={DEMO_DEVICE_DIRECTORY_URL}>
<meta property="og:title" content="All device profiles | devicekit">
<meta property="og:description" content={`Browse all ${DEVICES.length} devicekit device profiles.`}>
<meta property="og:url" content={DEMO_DEVICE_DIRECTORY_URL}>
<meta property="og:type" content="website">
<script type="application/ld+json" set:html={JSON.stringify({
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'devicekit device profiles',
description: `Browse all ${DEVICES.length} devicekit device profiles.`,
url: DEMO_DEVICE_DIRECTORY_URL,
numberOfItems: DEVICES.length,
})}></script>
</head>
<body class="device-directory-page">
<main class="device-directory">
<p class="device-directory__eyebrow">devicekit / frame</p>
<h1>All device profiles</h1>
<p class="device-directory__intro">Choose one of {DEVICES.length} iOS, Android and HarmonyOS device profiles to inspect its viewport, pixel ratio, safe areas and device frame.</p>
<p><a class="device-directory__home" href={new URL(DEMO_SITE_URL).pathname}>Open the device frame simulator</a></p>
<ul class="device-directory__list">
{DEVICES.map((device) => <li><a href={deviceHref(device.name)}>{device.name}</a><span>{device.os} · {device.screen.width} × {device.screen.height} · {device.pixelRatio}×</span></li>)}
</ul>
</main>
</body>
</html>
4 changes: 2 additions & 2 deletions packages/frame/demo/src/pages/sitemap.xml.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { DEVICES } from '@devicekit/devices'
import { DEMO_SITE_URL, deviceCanonicalUrl } from '../../../src/demo-device-seo.js'
import { DEMO_DEVICE_DIRECTORY_URL, DEMO_SITE_URL, deviceCanonicalUrl } from '../../../src/demo-device-seo.js'

export const prerender = true

export function GET(): Response {
const urls = [DEMO_SITE_URL, ...DEVICES.map(deviceCanonicalUrl)]
const urls = [DEMO_SITE_URL, DEMO_DEVICE_DIRECTORY_URL, ...DEVICES.map(deviceCanonicalUrl)]
const body = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.map((url) => ` <url><loc>${url}</loc></url>`).join('\n')}\n</urlset>\n`
return new Response(body, { headers: { 'Content-Type': 'application/xml' } })
}
55 changes: 55 additions & 0 deletions packages/frame/demo/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,61 @@ body {
line-height: 1.45;
}

.device-directory-link {
display: block;
margin: -8px 0 16px;
color: var(--demo-body-color);
font-size: 12px;
font-weight: 600;
}

.device-profile {
margin: 16px 0;
padding: 14px;
border: 1px solid var(--demo-panel-border);
border-radius: 10px;
background: color-mix(in srgb, var(--demo-panel-background) 92%, #f4f4f5);
}

.device-profile h2 {
margin: 0 0 10px;
font-size: 13px;
}

.device-profile dl { margin: 0; }
.device-profile dl > div { display: flex; justify-content: space-between; gap: 12px; padding: 4px 0; }
.device-profile dt { color: var(--demo-label-color); }
.device-profile dd { margin: 0; font-weight: 600; text-align: right; }

.device-directory-page {
display: block;
height: auto;
min-height: 100vh;
}

.device-directory {
width: min(100% - 32px, 960px);
margin: 0 auto;
padding: 64px 0;
}

.device-directory__eyebrow {
margin: 0 0 10px;
color: var(--demo-label-color);
font-size: 11px;
font-weight: 700;
letter-spacing: .09em;
text-transform: uppercase;
}

.device-directory h1 { margin: 0; font-size: clamp(28px, 5vw, 44px); letter-spacing: -.035em; }
.device-directory__intro { max-width: 720px; margin: 16px 0; color: var(--demo-note-color); font-size: 16px; }
.device-directory__home { color: var(--demo-body-color); font-weight: 600; }
.device-directory__list { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 10px; margin: 32px 0 0; padding: 0; list-style: none; }
.device-directory__list li { padding: 14px; border: 1px solid var(--demo-panel-border); border-radius: 10px; background: var(--demo-panel-background); }
.device-directory__list a { display: block; color: var(--demo-body-color); font-weight: 700; }
.device-directory__list span { display: block; margin-top: 5px; color: var(--demo-note-color); font-size: 12px; }

.panel__eyebrow,
.control-section__title,
.stage__label {
Expand Down
23 changes: 23 additions & 0 deletions packages/frame/src/demo-crawlability.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest'
// @ts-expect-error -- node:fs is available in the Vitest runtime only.
import { readFileSync } from 'node:fs'

const deviceDirectory = readFileSync('demo/src/pages/devices/index.astro', 'utf8')
const demoPage = readFileSync('demo/src/components/DemoPage.astro', 'utf8')

describe('demo crawlability', () => {
it('provides a static directory that links to every device detail page', () => {
expect(deviceDirectory).toContain('DEVICES.map')
expect(deviceDirectory).toContain('<a href={deviceHref(device.name)}>')
expect(deviceDirectory).toContain('new URL(devicePath(name), DEMO_SITE_URL).pathname')
})

it('renders the selected device facts and the directory link before JavaScript runs', () => {
expect(demoPage).toContain('id="device-directory"')
expect(demoPage).toContain('new URL(DEMO_DEVICE_DIRECTORY_URL).pathname')
expect(demoPage).toContain('Operating system')
expect(demoPage).toContain('CSS viewport')
expect(demoPage).toContain('Pixel ratio')
expect(demoPage).toContain('Release year')
})
})
3 changes: 2 additions & 1 deletion packages/frame/src/demo-device-seo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { PresetDeviceProfile } from '@devicekit/devices'
import { devicePath } from './demo-device-url.js'

export const DEMO_SITE_URL = 'https://echotechfe.github.io/devicekit/'
export const DEMO_DEVICE_DIRECTORY_URL = new URL('devices/', DEMO_SITE_URL).href

const OS_LABEL = { ios: 'iOS', android: 'Android', harmony: 'HarmonyOS' } as const

Expand All @@ -28,7 +29,7 @@ export function devicePageJsonLd(device: PresetDeviceProfile): Record<string, un
isPartOf: {
'@type': 'CollectionPage',
name: 'devicekit device profiles',
url: DEMO_SITE_URL,
url: DEMO_DEVICE_DIRECTORY_URL,
},
mainEntity: {
'@type': 'Thing',
Expand Down
6 changes: 5 additions & 1 deletion packages/frame/src/demo-page-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ describe('demoPageMetadata', () => {
expect(metadata.title).toBe('iPhone 18 Pro device frame simulator | devicekit')
expect(metadata.heading).toBe('iPhone 18 Pro device frame simulator')
expect(metadata.canonical).toBe('https://echotechfe.github.io/devicekit/devices/iphone-18-pro/')
expect(metadata.jsonLd).toMatchObject({ '@type': 'WebPage', url: metadata.canonical })
expect(metadata.jsonLd).toMatchObject({
'@type': 'WebPage',
url: metadata.canonical,
isPartOf: { url: 'https://echotechfe.github.io/devicekit/devices/' },
})
})

it('makes browser history restore homepage metadata after a device route', () => {
Expand Down
30 changes: 30 additions & 0 deletions scripts/device-pages.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import path from 'node:path'

const scriptsDir = path.dirname(fileURLToPath(import.meta.url))
const repoRoot = path.resolve(scriptsDir, '..')
const pagesPath = path.join(repoRoot, 'docs/device-pages.md')
const deviceNamesPath = path.join(repoRoot, 'packages/devices/src/device-names.generated.ts')

function deviceSlug(name) {
return name
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
}

test('docs/device-pages.md lists exactly one live URL for every device profile', () => {
const markdown = readFileSync(pagesPath, 'utf8')
const names = [...readFileSync(deviceNamesPath, 'utf8').matchAll(/^ \w+: '([^']+)',$/gm)].map((match) => match[1])
const urls = [...markdown.matchAll(/\((https:\/\/echotechfe\.github\.io\/devicekit\/devices\/[^)]+\/)\)/g)].map((match) => match[1])
const expectedUrls = names.map((name) => `https://echotechfe.github.io/devicekit/devices/${deviceSlug(name)}/`)

assert.ok(markdown.includes('pnpm generate:device-pages'))
assert.equal(urls.length, names.length)
assert.deepEqual(new Set(urls), new Set(expectedUrls))
})
38 changes: 38 additions & 0 deletions scripts/generate-device-pages.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'

const scriptsDir = path.dirname(fileURLToPath(import.meta.url))
const repoRoot = path.resolve(scriptsDir, '..')
const outputFile = path.join(repoRoot, 'docs/device-pages.md')
const checkOnly = process.argv.includes('--check')
const devicesModule = pathToFileURL(path.join(repoRoot, 'packages/devices/dist/presets/index.js')).href
const seoModule = pathToFileURL(path.join(repoRoot, 'packages/frame/dist/demo-device-seo.js')).href
const { DEVICES } = await import(devicesModule)
const { deviceCanonicalUrl } = await import(seoModule)

const rows = DEVICES.map((device) => `| ${device.name} | ${device.os} | ${device.screen.width} × ${device.screen.height} | ${device.pixelRatio}× | [Open demo](${deviceCanonicalUrl(device)}) |`).join('\n')
const content = `# Device demo pages

This generated index lists every device profile with its public DeviceKit demo URL. It is kept in the repository so the complete set of pages is visible on GitHub as well as in the live site directory and sitemap.

Do not edit this file by hand. Run \`pnpm generate:device-pages\` after changing the device table.

| Device | OS | CSS viewport | Pixel ratio | Live demo |
| --- | --- | ---: | ---: | --- |
${rows}
`

if (checkOnly) {
const current = readFileSync(outputFile, 'utf8')
if (current !== content) {
console.error('generate-device-pages: docs/device-pages.md is out of date.')
console.error('Run `pnpm generate:device-pages` and commit the result.')
process.exit(1)
}
console.log(`generate-device-pages: ${DEVICES.length} device page(s) are up to date.`)
} else {
writeFileSync(outputFile, content)
console.log(`generate-device-pages: wrote ${DEVICES.length} device page(s) to docs/device-pages.md`)
}
Loading