@nuxtseo/design-system 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -2
- package/app/app.config.ts +596 -0
- package/app/component-attributes.d.ts +8 -0
- package/app/components/README.md +38 -0
- package/app/components/brand/UiBrandMark.vue +63 -0
- package/app/components/brand/UiModuleIcon.vue +45 -0
- package/app/components/brand/UiProLettering.vue +26 -0
- package/app/components/brand/UiRangeTerrain.vue +311 -0
- package/app/components/brand/range-glyphs.ts +18 -0
- package/app/components/container/BrowserTab.vue +145 -0
- package/app/components/container/IntegrationRow.vue +120 -0
- package/app/components/container/KnowDoRails.vue +40 -0
- package/app/components/container/RailPanel.vue +93 -0
- package/app/components/container/UiAlert.vue +120 -0
- package/app/components/container/UiAppShell.vue +165 -0
- package/app/components/container/UiCard.vue +204 -0
- package/app/components/container/UiDisclosure.vue +43 -0
- package/app/components/container/UiEmptyState.vue +102 -0
- package/app/components/container/UiErrorState.vue +61 -0
- package/app/components/container/UiLoadingState.vue +20 -0
- package/app/components/container/UiPageHeader.vue +95 -0
- package/app/components/container/UiPopover.vue +102 -0
- package/app/components/container/UiSampleDataOverlay.vue +47 -0
- package/app/components/container/UiSectionHeader.vue +83 -0
- package/app/components/container/UiSummaryStrip.vue +40 -0
- package/app/components/container/UiToast.vue +230 -0
- package/app/components/container/UiToaster.vue +271 -0
- package/app/components/container/UiWidgetState.vue +143 -0
- package/app/components/container/ui-nav-drawer.ts +48 -0
- package/app/components/data/ActionRail.vue +152 -0
- package/app/components/data/FacetStrip.vue +93 -0
- package/app/components/data/UiBarCell.vue +37 -0
- package/app/components/data/UiBarChart.vue +281 -0
- package/app/components/data/UiChartAnnotationControls.vue +74 -0
- package/app/components/data/UiChartAnnotations.vue +163 -0
- package/app/components/data/UiChartBrushOverlay.vue +96 -0
- package/app/components/data/UiChartDataReveal.vue +81 -0
- package/app/components/data/UiChartFrame.vue +123 -0
- package/app/components/data/UiChartTooltip.vue +81 -0
- package/app/components/data/UiCoverageNote.vue +90 -0
- package/app/components/data/UiCwvSparkline.vue +188 -0
- package/app/components/data/UiDataList.vue +276 -0
- package/app/components/data/UiDataTableSection.vue +232 -0
- package/app/components/data/UiDistributionBars.vue +164 -0
- package/app/components/data/UiDonutChart.vue +132 -0
- package/app/components/data/UiDoorwayColumn.vue +108 -0
- package/app/components/data/UiDoorwayGrid.vue +58 -0
- package/app/components/data/UiDoorwayRow.vue +83 -0
- package/app/components/data/UiEntitySummary.vue +261 -0
- package/app/components/data/UiFactsGrid.vue +107 -0
- package/app/components/data/UiImpactMeter.vue +33 -0
- package/app/components/data/UiIssueRow.vue +149 -0
- package/app/components/data/UiLineChart.vue +431 -0
- package/app/components/data/UiMetricBarPopover.vue +121 -0
- package/app/components/data/UiMetricLabel.vue +30 -0
- package/app/components/data/UiMetricStat.vue +73 -0
- package/app/components/data/UiMetricToggle.vue +156 -0
- package/app/components/data/UiMetricsRow.vue +64 -0
- package/app/components/data/UiPositionMetric.vue +53 -0
- package/app/components/data/UiRelatedFixes.vue +113 -0
- package/app/components/data/UiRichText.vue +325 -0
- package/app/components/data/UiScatterPlot.vue +144 -0
- package/app/components/data/UiSeverityMarker.vue +30 -0
- package/app/components/data/UiSparkColumns.vue +160 -0
- package/app/components/data/UiSparkline.vue +681 -0
- package/app/components/data/UiStat.vue +445 -0
- package/app/components/data/UiStats.vue +76 -0
- package/app/components/data/UiTable.vue +517 -0
- package/app/components/data/UiTableExport.vue +40 -0
- package/app/components/data/UiTableFrame.vue +149 -0
- package/app/components/data/UiTablePagination.vue +37 -0
- package/app/components/data/UiTableShell.vue +48 -0
- package/app/components/data/UiTableTd.vue +39 -0
- package/app/components/data/UiTableTh.vue +61 -0
- package/app/components/data/UiTopEntityStackChart.vue +393 -0
- package/app/components/data/cells/UiTableDash.vue +3 -0
- package/app/components/data/cells/UiTableHeaderCell.vue +72 -0
- package/app/components/data/cells/UiTableMetricCell.vue +72 -0
- package/app/components/data/cells/UiTablePathCell.vue +76 -0
- package/app/components/data/cells/UiTableScoreTile.vue +49 -0
- package/app/components/data/cells/UiTableTrendCell.vue +41 -0
- package/app/components/element/UiAtmosphere.vue +64 -0
- package/app/components/element/UiAuthHeading.vue +24 -0
- package/app/components/element/UiAuthProviders.vue +46 -0
- package/app/components/element/UiBrandIcon.vue +39 -0
- package/app/components/element/UiButton.vue +549 -0
- package/app/components/element/UiCelebration.vue +65 -0
- package/app/components/element/UiChannelChips.vue +62 -0
- package/app/components/element/UiChatQuickLink.vue +319 -0
- package/app/components/element/UiChip.vue +101 -0
- package/app/components/element/UiConfirmDialog.vue +84 -0
- package/app/components/element/UiCopyButton.vue +90 -0
- package/app/components/element/UiDitherAvatar.vue +177 -0
- package/app/components/element/UiDotLabel.vue +19 -0
- package/app/components/element/UiEjectMenu.vue +181 -0
- package/app/components/element/UiEjectOverlay.vue +29 -0
- package/app/components/element/UiFavicon.vue +235 -0
- package/app/components/element/UiFaviconStack.vue +84 -0
- package/app/components/element/UiFilterMenu.vue +324 -0
- package/app/components/element/UiFocusedSurface.vue +129 -0
- package/app/components/element/UiHealthDot.vue +32 -0
- package/app/components/element/UiHelpLabel.vue +28 -0
- package/app/components/element/UiIcon.vue +22 -0
- package/app/components/element/UiInput.vue +35 -0
- package/app/components/element/UiLogoPlausible.vue +12 -0
- package/app/components/element/UiLogoPro.vue +25 -0
- package/app/components/element/UiLogoUmami.vue +12 -0
- package/app/components/element/UiNavIcon.vue +107 -0
- package/app/components/element/UiNavList.vue +156 -0
- package/app/components/element/UiOnboardingAnswers.vue +125 -0
- package/app/components/element/UiOverflowCount.vue +31 -0
- package/app/components/element/UiProBadge.vue +19 -0
- package/app/components/element/UiProgressCircle.vue +79 -0
- package/app/components/element/UiProgressPercent.vue +35 -0
- package/app/components/element/UiRelativeTime.vue +58 -0
- package/app/components/element/UiScrollFade.vue +16 -0
- package/app/components/element/UiSelect.vue +55 -0
- package/app/components/element/UiSeverityDot.vue +27 -0
- package/app/components/element/UiSiteFavicon.vue +24 -0
- package/app/components/element/UiSkeleton.vue +148 -0
- package/app/components/element/UiSlotText.vue +65 -0
- package/app/components/element/UiSourceLogos.vue +113 -0
- package/app/components/element/UiSourceRail.vue +71 -0
- package/app/components/element/UiStaticTerminal.vue +107 -0
- package/app/components/element/UiStatusBadge.vue +62 -0
- package/app/components/element/UiSyncDot.vue +46 -0
- package/app/components/element/UiTabs.vue +245 -0
- package/app/components/element/UiTogglePill.vue +86 -0
- package/app/components/element/UiTooltip.vue +390 -0
- package/app/components/element/UiTooltipGrid.vue +457 -0
- package/app/components/element/UiTrend.vue +184 -0
- package/app/components/element/UiTrialBenefits.vue +71 -0
- package/app/components/element/UiUrlLabel.vue +75 -0
- package/app/components/element/UiValenceDot.vue +37 -0
- package/app/components/element/UiWizardModal.vue +284 -0
- package/app/components/element/UiWizardNav.vue +59 -0
- package/app/components/element/UiWizardProgress.vue +78 -0
- package/app/components/element/UiWizardShell.vue +43 -0
- package/app/components/element/eject-menu.ts +359 -0
- package/app/components/element/eject-tracker.ts +34 -0
- package/app/components/element/onboarding-answers.ts +68 -0
- package/app/components/element/onboarding-intents.ts +19 -0
- package/app/composables/dataVizColors.ts +350 -0
- package/app/composables/formatting.ts +252 -0
- package/app/composables/semanticColors.ts +80 -0
- package/app/composables/useActivityOverlay.ts +55 -0
- package/app/composables/useAppShellNav.ts +21 -0
- package/app/composables/useChartBrush.ts +221 -0
- package/app/composables/useChartHover.ts +153 -0
- package/app/composables/useChartTickPlan.ts +186 -0
- package/app/composables/useCloseOnNavigation.ts +7 -0
- package/app/composables/useConfirm.ts +38 -0
- package/app/composables/useEdgeFade.ts +37 -0
- package/app/composables/useEject.ts +69 -0
- package/app/composables/useEntranceMotion.ts +22 -0
- package/app/composables/useFaviconBacking.ts +164 -0
- package/app/composables/useGridCols.ts +39 -0
- package/app/composables/useIssueOverlay.ts +43 -0
- package/app/composables/useOnboardingService.ts +106 -0
- package/app/composables/useOpenedOnce.ts +19 -0
- package/app/composables/useScrollSpy.ts +178 -0
- package/app/composables/useSquircleFallback.ts +21 -0
- package/app/composables/useWizardRoute.ts +123 -0
- package/app/plugins/favicon.ts +29 -0
- package/app/plugins/lazy-motion.ts +22 -0
- package/app/plugins/selection-highlight.client.ts +176 -0
- package/app/shared/cli.ts +10 -0
- package/app/shared/error-state.ts +150 -0
- package/app/shared/icons.ts +1 -0
- package/app/shared/motion.ts +1 -0
- package/app/shared/nav.ts +38 -0
- package/app/shared/navigation.ts +5 -0
- package/app/shared/static-terminal.ts +14 -0
- package/app/shared/table-export.ts +36 -0
- package/app/shared/table.ts +105 -0
- package/app/shared/ui-icons.ts +1 -0
- package/app/shared/view-state.ts +85 -0
- package/app/shared/widget-failure.ts +126 -0
- package/app/utils/chartAnnotations.ts +326 -0
- package/app/utils/countries.ts +337 -0
- package/app/utils/export-download.ts +100 -0
- package/app/utils/favicon-fallback.ts +12 -0
- package/app/utils/icon-color.ts +105 -0
- package/app/utils/number.ts +74 -0
- package/app/utils/render-link.ts +14 -0
- package/app/utils/slot-text.ts +18 -0
- package/app/utils/squircle.ts +154 -0
- package/app/utils/topEntityStack.ts +197 -0
- package/app/utils/vue.ts +57 -0
- package/build/design-system.ts +47 -0
- package/build/icon-server-bundle.ts +276 -0
- package/build/runtime/icon-server-bundle.ts +29 -0
- package/css/global.css +1892 -0
- package/nuxt.config.ts +132 -0
- package/package.json +86 -3
- package/public/brand-icon-dark.png +0 -0
- package/public/brand-icon-light.png +0 -0
- package/public/favicon-dark.png +0 -0
- package/public/favicon-light.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/fonts/README.md +31 -0
- package/public/fonts/fira-code-OFL.txt +93 -0
- package/public/fonts/fira-code-var-latin.woff2 +0 -0
- package/public/fonts/hubot-sans-OFL.txt +93 -0
- package/public/fonts/hubot-sans-var-latin.woff2 +0 -0
- package/public/icon-dark.png +0 -0
- package/public/icon.png +0 -0
- package/public/touch-icon-dark.png +0 -0
- package/public/touch-icon.png +0 -0
- package/server/api/favicon.get.ts +122 -0
- package/server/utils/favicon-source.ts +51 -0
- package/shared/contracts/feedback.ts +42 -0
- package/shared/contracts/index.ts +1 -0
- package/shared/delivery-channels.ts +53 -0
- package/shared/icons/index.ts +143 -0
- package/shared/icons/registry.ts +347 -0
- package/shared/icons/ui-slots.ts +58 -0
- package/shared/markdown.ts +44 -0
- package/shared/motion.ts +120 -0
- package/shared/rangi.ts +60 -0
- package/shared/rich-text.ts +34 -0
- package/shared/ui-icons.ts +6 -0
- package/shared/utils/urls.ts +64 -0
- package/types/index.ts +1 -0
- package/types/ui.ts +339 -0
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { defineNuxtConfig } from 'nuxt/config'
|
|
2
|
+
import { join } from 'pathe'
|
|
3
|
+
import { UI_ICON_SLOTS } from './shared/icons/ui-slots'
|
|
4
|
+
|
|
5
|
+
const currentDir = import.meta.dirname
|
|
6
|
+
|
|
7
|
+
// Design system layer: design language extended by every app — UI primitives, tokens, fonts,
|
|
8
|
+
// motion, chart helpers, formatters, design vocabularies (Status, severity), cross-cutting UI
|
|
9
|
+
// affordances (eject, feedback widget contracts).
|
|
10
|
+
//
|
|
11
|
+
// Per ADR-0042 (pragmatic rule): MAY know domain shapes; MUST NOT import runtime services
|
|
12
|
+
// (no useFetch, Drizzle, Caller, billing, auth, useRuntimeConfig). Storybook test: renders with
|
|
13
|
+
// prop fixtures alone. Placement contract for components: ADR-0029. Components MUST NOT be
|
|
14
|
+
// prefixed Pro/Admin/Tool/Docs/Learn — the layer name is the scope. Internal taxonomy:
|
|
15
|
+
// components/{container,data,element}/.
|
|
16
|
+
|
|
17
|
+
export default defineNuxtConfig({
|
|
18
|
+
imports: { autoImport: false },
|
|
19
|
+
|
|
20
|
+
nitro: {
|
|
21
|
+
imports: { autoImport: false },
|
|
22
|
+
alias: {
|
|
23
|
+
// `@nuxt/image@2.1.0` reaches `useRuntimeConfig` through
|
|
24
|
+
// `nitro/runtime-config` — the nitro v3 package — in
|
|
25
|
+
// `runtime/server/routes/_ipx.js` and `runtime/server/utils/image.js`. It
|
|
26
|
+
// declares no peer dependency for it, and this project runs Nuxt 4.5 on
|
|
27
|
+
// nitropack v2, where that specifier does not exist. Without this alias
|
|
28
|
+
// every app builds a server bundle importing a package that is not
|
|
29
|
+
// installed and boots to `Cannot find package 'nitro'`.
|
|
30
|
+
//
|
|
31
|
+
// A `patchedDependencies` entry cannot express this. `pnpm patch`
|
|
32
|
+
// extracts a PRISTINE copy that imports from `#imports`, so the diff comes
|
|
33
|
+
// out empty — the bad bytes live in pnpm's unpacked store layer and
|
|
34
|
+
// re-materialise on any install, three times in one session, machine-wide
|
|
35
|
+
// across every checkout sharing the store. An alias is immune to that
|
|
36
|
+
// because it never depends on what is on disk.
|
|
37
|
+
//
|
|
38
|
+
// Remove when the app moves to nitro v3, or when @nuxt/image ships the v2
|
|
39
|
+
// compatibility path its sibling @nuxt/scripts@2.0.0-beta.2 already
|
|
40
|
+
// carries (`resolveNitroImport` probing `nitro/app`, `nitro/cache`,
|
|
41
|
+
// `nitro/h3`, `nitro/runtime-config`, falling back to `nitropack/runtime`).
|
|
42
|
+
'nitro/runtime-config': 'nitropack/runtime',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Nuxt UI collects its app-config slots into the icon client bundle during
|
|
47
|
+
// module setup. Declare the semantic mapping here as well as in app.config
|
|
48
|
+
// so its default Lucide slots never become a second implicit allowlist.
|
|
49
|
+
appConfig: {
|
|
50
|
+
ui: { icons: UI_ICON_SLOTS },
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
modules: [
|
|
54
|
+
// Before `@nuxt/ui`: it writes the icon aliases and client bundle that
|
|
55
|
+
// @nuxt/icon reads. `designSystem` options live in build/design-system.ts.
|
|
56
|
+
join(currentDir, './build/design-system'),
|
|
57
|
+
'@comark/nuxt',
|
|
58
|
+
'@nuxt/fonts',
|
|
59
|
+
'@nuxt/image',
|
|
60
|
+
'@nuxt/scripts',
|
|
61
|
+
'motion-v/nuxt',
|
|
62
|
+
'reka-ui/nuxt',
|
|
63
|
+
'@nuxt/ui',
|
|
64
|
+
],
|
|
65
|
+
|
|
66
|
+
css: [
|
|
67
|
+
join(currentDir, './css/global.css'),
|
|
68
|
+
],
|
|
69
|
+
|
|
70
|
+
components: {
|
|
71
|
+
dirs: [
|
|
72
|
+
{
|
|
73
|
+
path: join(currentDir, './app/components'),
|
|
74
|
+
pathPrefix: false,
|
|
75
|
+
// Higher priority so design-system primitives win shadow conflicts against root
|
|
76
|
+
// app/components/* of the same basename (e.g. content/Alert.vue, owned by the
|
|
77
|
+
// content team — left in place; design-system's Alert is the canonical one).
|
|
78
|
+
priority: 10,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Canonical type stack for every surface that extends this layer (root app
|
|
84
|
+
// + apps/brand-kit brand-kit). Keep in sync via this single block —
|
|
85
|
+
// don't redeclare in root or app configs.
|
|
86
|
+
fonts: {
|
|
87
|
+
// Both families are global. CSS variable discovery cannot add another
|
|
88
|
+
// face, so avoid parsing every component style during each Vite build.
|
|
89
|
+
processCSSVariables: false,
|
|
90
|
+
families: [
|
|
91
|
+
// Keep build inputs in this layer. Provider downloads make every app
|
|
92
|
+
// build depend on Google Fonts availability after compilation finishes.
|
|
93
|
+
// Hubot Sans keeps variable weight and width axes for the design tokens.
|
|
94
|
+
{
|
|
95
|
+
name: 'Hubot Sans',
|
|
96
|
+
src: '/fonts/hubot-sans-var-latin.woff2',
|
|
97
|
+
weight: '200 900',
|
|
98
|
+
stretch: '75% 125%',
|
|
99
|
+
style: 'normal',
|
|
100
|
+
global: true,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'Fira Code',
|
|
104
|
+
src: '/fonts/fira-code-var-latin.woff2',
|
|
105
|
+
weight: '400 500',
|
|
106
|
+
style: 'normal',
|
|
107
|
+
global: true,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
icon: {
|
|
113
|
+
// build/design-system.ts adds the role aliases and the chosen set's client
|
|
114
|
+
// bundle. Ship only that set's resolved role icons. Explicit raw ids outside
|
|
115
|
+
// that curated bundle resolve through Iconify in the browser. An app that
|
|
116
|
+
// installs `build/icon-server-bundle.ts` renders them on the server too.
|
|
117
|
+
provider: 'iconify',
|
|
118
|
+
serverBundle: false,
|
|
119
|
+
clientBundle: {
|
|
120
|
+
scan: false,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
ui: {
|
|
125
|
+
experimental: { componentDetection: true },
|
|
126
|
+
content: true,
|
|
127
|
+
theme: {
|
|
128
|
+
colors: ['primary', 'secondary', 'tertiary', 'info', 'success', 'warning', 'error', 'pro'],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,89 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxtseo/design-system",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Nuxt SEO design system layer: UI primitives, tokens, fonts, motion, and chart helpers. Extend it from a Nuxt app.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://nuxtseo.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/harlan-zw/nuxtseo.com.git",
|
|
11
|
+
"directory": "layers/design-system"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"nuxt",
|
|
15
|
+
"nuxt-layer",
|
|
16
|
+
"design-system",
|
|
17
|
+
"nuxt-ui"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./nuxt.config.ts",
|
|
21
|
+
"./*": "./*",
|
|
22
|
+
"./app/*": "./app/*.ts",
|
|
23
|
+
"./app/*.vue": "./app/*.vue",
|
|
24
|
+
"./components/*": "./app/components/*.ts",
|
|
25
|
+
"./components/*.vue": "./app/components/*.vue",
|
|
26
|
+
"./composables/*": "./app/composables/*.ts",
|
|
27
|
+
"./package.json": "./package.json",
|
|
28
|
+
"./server/*": "./server/*.ts",
|
|
29
|
+
"./shared/*": "./shared/*.ts"
|
|
30
|
+
},
|
|
31
|
+
"main": "./nuxt.config.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"app",
|
|
34
|
+
"build",
|
|
35
|
+
"css",
|
|
36
|
+
"nuxt.config.ts",
|
|
37
|
+
"public",
|
|
38
|
+
"server",
|
|
39
|
+
"shared",
|
|
40
|
+
"types"
|
|
41
|
+
],
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"nuxt": "^4.6.0",
|
|
44
|
+
"typescript": ">=5.8.0 <7",
|
|
45
|
+
"vue": "^3.5.0"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@comark/nuxt": "0.7.0",
|
|
49
|
+
"@iconify-json/hugeicons": "^1.2.34",
|
|
50
|
+
"@iconify-json/logos": "^1.2.14",
|
|
51
|
+
"@iconify-json/lucide": "^1.2.130",
|
|
52
|
+
"@iconify-json/simple-icons": "^1.2.95",
|
|
53
|
+
"@iconify-json/svg-spinners": "^1.2.4",
|
|
54
|
+
"@iconify/vue": "^5.0.1",
|
|
55
|
+
"@nuxt/fonts": "^1.0.1",
|
|
56
|
+
"@nuxt/icon": "^2.5.1",
|
|
57
|
+
"@nuxt/image": "^2.1.0",
|
|
58
|
+
"@nuxt/scripts": "2.0.0-beta.14",
|
|
59
|
+
"@nuxt/ui": "^4.11.1",
|
|
60
|
+
"@tanstack/table-core": "^9.2.4",
|
|
61
|
+
"@tanstack/vue-table": "^9.2.4",
|
|
62
|
+
"@unovis/ts": "^1.6.7",
|
|
63
|
+
"@unovis/vue": "^1.6.7",
|
|
64
|
+
"@vueuse/core": "^14.4.0",
|
|
65
|
+
"motion-v": "2.4.2",
|
|
66
|
+
"nuxtseo-shared": "^6.0.2",
|
|
67
|
+
"pathe": "^2.0.3",
|
|
68
|
+
"rangi": "^2.2.0",
|
|
69
|
+
"reka-ui": "^2.10.4",
|
|
70
|
+
"tailwindcss": "^4.3.3",
|
|
71
|
+
"zod": "^4.6.7"
|
|
72
|
+
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"ai": "^7.0.122",
|
|
75
|
+
"nuxt": "4.6.1",
|
|
76
|
+
"sharp": "^0.35.4",
|
|
77
|
+
"site-typecheck": "0.0.0",
|
|
78
|
+
"vue": "^3.5.43"
|
|
79
|
+
},
|
|
80
|
+
"publishConfig": {
|
|
81
|
+
"access": "public"
|
|
82
|
+
},
|
|
83
|
+
"scripts": {
|
|
84
|
+
"nuxt:prepare": "nuxt prepare",
|
|
85
|
+
"typecheck": "nuxt prepare && site-typecheck -b --noEmit --force",
|
|
86
|
+
"brand:assets": "node --experimental-strip-types scripts/brand-assets.ts",
|
|
87
|
+
"typecheck:legacy": "nuxt typecheck"
|
|
88
|
+
}
|
|
6
89
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Build fonts
|
|
2
|
+
|
|
3
|
+
The design system owns these font files. Nuxt copies them into every app that
|
|
4
|
+
extends this layer. Builds never download fonts from an external provider.
|
|
5
|
+
|
|
6
|
+
The Hubot Sans file is a latin/latin-ext subset of the official v1.0.1 release
|
|
7
|
+
face (`HubotSans[slnt,wdth,wght].woff2`). It keeps the weight axis (200 to 900)
|
|
8
|
+
and the width axis (75% to 125%) for the design tokens. The slant axis is
|
|
9
|
+
pinned to 0 and dropped, and glyphs outside latin and latin-ext are removed,
|
|
10
|
+
so the transfer payload stays under 100KB
|
|
11
|
+
(`tests/design-system-hubot-font-subset.feature.test.ts` holds this contract).
|
|
12
|
+
|
|
13
|
+
To regenerate it, run a HarfBuzz subset over the release face with
|
|
14
|
+
`variationAxes: { wght: { min: 200, max: 900 }, wdth: { min: 75, max: 125 }, slnt: 0 }`
|
|
15
|
+
and the Google Fonts latin plus latin-ext unicode ranges.
|
|
16
|
+
|
|
17
|
+
The subset is a Modified Version under the OFL, so it must not present the
|
|
18
|
+
Reserved Font Name "Hubot". Override name records 1, 4, and 6 (plus 16 when
|
|
19
|
+
present) to a family name without "Hubot", for example "Nuxt SEO Sans" with
|
|
20
|
+
PostScript name "NuxtSEOSans-ExtraLight". Keep the copyright record verbatim;
|
|
21
|
+
it carries the reservation notice. The subset test holds this contract.
|
|
22
|
+
|
|
23
|
+
The Fira Code file contains the Latin subset. Nuxt exposes weights 400 through
|
|
24
|
+
500 and the normal style.
|
|
25
|
+
|
|
26
|
+
The alert email shell also uses Hubot Sans from the stable URL
|
|
27
|
+
`https://nuxtseo.com/fonts/hubot-sans-var-latin.woff2`.
|
|
28
|
+
|
|
29
|
+
Both fonts use the SIL Open Font License 1.1. Keep each font with its matching
|
|
30
|
+
license file. Hubot Sans release source:
|
|
31
|
+
`https://github.com/github/hubot-sans/releases/tag/v1.0.1`.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright (c) 2014, The Fira Code Project Authors (https://github.com/tonsky/FiraCode)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
https://openfontlicense.org
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2021 The Hubot Sans Project Authors (https://github.com/github/hubot-sans), with Reserved Font Name "Hubot"
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
https://openfontlicense.org
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
|
Binary file
|
package/public/icon.png
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createError, defineEventHandler, getQuery, getRequestHeader, setResponseHeader, setResponseStatus } from 'h3'
|
|
2
|
+
import { resolveStoredFavicon } from '../utils/favicon-source'
|
|
3
|
+
/**
|
|
4
|
+
* Same-origin favicon proxy. UiFavicon renders this instead of hitting Google's
|
|
5
|
+
* s2 endpoint directly so the image is (a) cacheable on our edge, (b) decoupled
|
|
6
|
+
* from leaking every user's site list to Google per render, and crucially
|
|
7
|
+
* (c) same-origin — which lets the client sample the favicon on a canvas without
|
|
8
|
+
* a cross-origin taint, the basis for UiFavicon's adaptive backing (see
|
|
9
|
+
* useFaviconBacking). We only ever fetch Google with the domain as a query
|
|
10
|
+
* param, never the supplied host directly, so there is no SSRF surface; the
|
|
11
|
+
* format guard just rejects junk input.
|
|
12
|
+
*
|
|
13
|
+
* TWO SOURCES, in order. A Site the Crawl has reached is served from our own
|
|
14
|
+
* store — the Crawl fetches the home page's icon links anyway, so the bytes are
|
|
15
|
+
* the ones the Site actually serves today. Everything else (a competitor domain,
|
|
16
|
+
* a Site added minutes ago, any domain a marketing page names) falls back to
|
|
17
|
+
* Google's `s2` copy, which is the only source we have for a domain we do not
|
|
18
|
+
* crawl.
|
|
19
|
+
*
|
|
20
|
+
* The fallback is where staleness lives and it is not ours to fix: measured
|
|
21
|
+
* 2026-09-23, Google's copy of gscdump.com's icon and the icon that Site serves
|
|
22
|
+
* are different files, and our 30-day edge copy matched Google's exactly. No
|
|
23
|
+
* cache purge moves it. Crawling the Site is the fix, which is what the store
|
|
24
|
+
* branch is.
|
|
25
|
+
*/
|
|
26
|
+
// 1×1 FULLY TRANSPARENT PNG. The previous constant claimed to be this and was
|
|
27
|
+
// not: its single pixel decoded to `R=0 G=255 B=0 A=127`, so every mail client
|
|
28
|
+
// scaled a half-opaque pure-green swatch to 16px and painted it beside the
|
|
29
|
+
// hostname — under a warning headline, where it read as a health dot. Verify
|
|
30
|
+
// with a decode, never by looking: at 1×1 the difference is invisible in source
|
|
31
|
+
// and obvious in an inbox.
|
|
32
|
+
// `?fallback=blank` callers get this with a 200 instead of
|
|
33
|
+
// a 404 — an <img> that 404s paints a broken-image glyph in clients with no
|
|
34
|
+
// scripted onerror to fall back with (mail, notably), and blank space beats a
|
|
35
|
+
// broken icon there. UiFavicon also requests the blank response, then detects
|
|
36
|
+
// its 1×1 size and swaps in the initials box without logging a failed request.
|
|
37
|
+
const BLANK_PNG = Uint8Array.from(
|
|
38
|
+
atob('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII='),
|
|
39
|
+
c => c.charCodeAt(0),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
export default defineEventHandler(async (event) => {
|
|
43
|
+
const query = getQuery(event)
|
|
44
|
+
const blankOnMiss = query.fallback === 'blank'
|
|
45
|
+
const contentHash = query.hash
|
|
46
|
+
const raw = String(query.domain ?? '')
|
|
47
|
+
.replace(/^sc-domain:/, '')
|
|
48
|
+
.replace(/^https?:\/\//, '')
|
|
49
|
+
.replace(/\/.*$/, '')
|
|
50
|
+
.trim()
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
|
|
53
|
+
function miss() {
|
|
54
|
+
if (!blankOnMiss) {
|
|
55
|
+
setResponseStatus(event, 404)
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
setResponseHeader(event, 'Content-Type', 'image/png')
|
|
59
|
+
setResponseHeader(event, 'Cache-Control', 'public, max-age=604800, s-maxage=2592000, immutable')
|
|
60
|
+
return BLANK_PNG
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!raw || !/^[a-z0-9.-]+\.[a-z]{2,}$/.test(raw)) {
|
|
64
|
+
if (blankOnMiss)
|
|
65
|
+
return miss()
|
|
66
|
+
throw createError({ statusCode: 400, statusMessage: 'invalid domain' })
|
|
67
|
+
}
|
|
68
|
+
if (contentHash !== undefined && (typeof contentHash !== 'string' || !/^[a-f0-9]{64}$/.test(contentHash)))
|
|
69
|
+
throw createError({ statusCode: 400, statusMessage: 'invalid favicon hash' })
|
|
70
|
+
|
|
71
|
+
// Our own capture wins whenever we have one. A failed lookup is not an error
|
|
72
|
+
// worth surfacing: the Google fallback below is a complete answer, so a store
|
|
73
|
+
// that is down shows the icon everything showed before the store existed.
|
|
74
|
+
const stored = await resolveStoredFavicon(event, raw, contentHash).catch((silentCatchError: unknown) => {
|
|
75
|
+
console.warn('[silent-catch] layers/design-system/server/api/favicon.get.ts:stored-lookup', silentCatchError)
|
|
76
|
+
return null
|
|
77
|
+
})
|
|
78
|
+
if (stored) {
|
|
79
|
+
const etag = `"${stored.contentHash}"`
|
|
80
|
+
setResponseHeader(event, 'ETag', etag)
|
|
81
|
+
// NOT `immutable`. These bytes can change the day a Site rebrands, and the
|
|
82
|
+
// whole point of storing them is that the change reaches the reader. The
|
|
83
|
+
// ETag makes the revalidation a 304 rather than a re-download.
|
|
84
|
+
setResponseHeader(event, 'Cache-Control', contentHash
|
|
85
|
+
? 'public, max-age=31536000, s-maxage=31536000, immutable'
|
|
86
|
+
: 'public, max-age=3600, s-maxage=86400, must-revalidate')
|
|
87
|
+
if (getRequestHeader(event, 'if-none-match') === etag) {
|
|
88
|
+
setResponseStatus(event, 304)
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
setResponseHeader(event, 'Content-Type', stored.contentType)
|
|
92
|
+
return stored.bytes
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A dated request must answer from its captured bytes. Google's current
|
|
96
|
+
// copy would show the wrong mark as the "before" state.
|
|
97
|
+
if (contentHash) {
|
|
98
|
+
setResponseStatus(event, 404)
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const upstream = `https://www.google.com/s2/favicons?domain=${encodeURIComponent(raw)}&sz=128`
|
|
103
|
+
|
|
104
|
+
// A missing favicon is an expected outcome, not an infra failure: plenty of
|
|
105
|
+
// domains have none, and the upstream fetch can also fail on DNS/network. In
|
|
106
|
+
// every such case we want UiFavicon's <img> onerror to fall through to the
|
|
107
|
+
// initials box — so we set a non-2xx status and return empty rather than
|
|
108
|
+
// throwing, which would surface as an unhandled error in Sentry.
|
|
109
|
+
const res = await fetch(upstream).catch((silentCatchError: unknown) => {
|
|
110
|
+
console.warn('[silent-catch] layers/design-system/server/api/favicon.get.ts:30', silentCatchError)
|
|
111
|
+
return null
|
|
112
|
+
})
|
|
113
|
+
if (!res?.ok)
|
|
114
|
+
return miss()
|
|
115
|
+
|
|
116
|
+
setResponseHeader(event, 'Content-Type', res.headers.get('content-type') ?? 'image/png')
|
|
117
|
+
// Google's copy of a domain's icon changes on Google's schedule, which we
|
|
118
|
+
// cannot see or influence, so caching it hard costs nothing we could have
|
|
119
|
+
// served better. A Site we crawl never reaches this branch.
|
|
120
|
+
setResponseHeader(event, 'Cache-Control', 'public, max-age=604800, s-maxage=2592000, immutable')
|
|
121
|
+
return new Uint8Array(await res.arrayBuffer())
|
|
122
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where `/api/favicon` looks before it falls back to Google.
|
|
3
|
+
*
|
|
4
|
+
* The endpoint ships in the design system, so both the site worker and the pro
|
|
5
|
+
* worker build it, and only pro holds the Sites tables and the R2 bucket that
|
|
6
|
+
* the Crawl writes captured marks into. Rather than import pro from here (the
|
|
7
|
+
* dependency runs the other way), the pro worker registers a resolver at cold
|
|
8
|
+
* start and the endpoint asks for one. A worker that registers nothing keeps
|
|
9
|
+
* exactly the behaviour this endpoint had before the store existed.
|
|
10
|
+
*
|
|
11
|
+
* Same registration shape as the Timeline's `defineEntryKind`: a nitro plugin
|
|
12
|
+
* in the owning layer fills a registry the shared code reads.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { H3Event } from 'h3'
|
|
16
|
+
|
|
17
|
+
export interface StoredFavicon {
|
|
18
|
+
bytes: Uint8Array
|
|
19
|
+
contentType: string
|
|
20
|
+
/** SHA-256 of the bytes, served as the `ETag` so a re-fetch after a rebrand is one 304. */
|
|
21
|
+
contentHash: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Takes the request event, not just the domain.
|
|
26
|
+
*
|
|
27
|
+
* The resolver reads D1, and this codebase's `useDb()` only attaches the
|
|
28
|
+
* session, the instruments and the 2-second first-query guard when it is given
|
|
29
|
+
* an event. A signature that could not carry one would have put an
|
|
30
|
+
* unguarded, uninstrumented read on a public endpoint that fires once per
|
|
31
|
+
* avatar on a portfolio page.
|
|
32
|
+
*/
|
|
33
|
+
export type FaviconSource = (event: H3Event, domain: string, contentHash?: string) => Promise<StoredFavicon | null>
|
|
34
|
+
|
|
35
|
+
let registered: FaviconSource | null = null
|
|
36
|
+
|
|
37
|
+
/** Called once per worker from the owning layer's nitro plugin. */
|
|
38
|
+
export function registerFaviconSource(source: FaviconSource): void {
|
|
39
|
+
registered = source
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The stored mark for a domain, or null when this worker has no store, the
|
|
44
|
+
* domain is not a Site we have crawled, or the lookup failed. Never throws: a
|
|
45
|
+
* store that is down must degrade to the Google fallback, not to a broken image.
|
|
46
|
+
*/
|
|
47
|
+
export async function resolveStoredFavicon(event: H3Event, domain: string, contentHash?: string): Promise<StoredFavicon | null> {
|
|
48
|
+
if (!registered)
|
|
49
|
+
return null
|
|
50
|
+
return registered(event, domain, contentHash)
|
|
51
|
+
}
|