@tanstack/devtools
Advanced tools
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import { DevtoolsProvider, PiPProvider } from '../chunk/AP5L3KAF.js'; | ||
| import '../chunk/BNGI36V3.js'; | ||
| import { render, createComponent, Portal } from 'solid-js/web'; | ||
| import { lazy } from 'solid-js'; | ||
| import { ClientEventBus } from '@tanstack/devtools-event-bus/client'; | ||
| function mountDevtools(options) { | ||
| const { | ||
| el, | ||
| plugins, | ||
| config, | ||
| eventBusConfig, | ||
| onSetPlugins | ||
| } = options; | ||
| const eventBus = new ClientEventBus(eventBusConfig); | ||
| eventBus.start(); | ||
| const Devtools = lazy(() => import('../devtools/4QORE6HP.js')); | ||
| const dispose = render(() => createComponent(DevtoolsProvider, { | ||
| plugins, | ||
| config, | ||
| onSetPlugins, | ||
| get children() { | ||
| return createComponent(PiPProvider, { | ||
| get children() { | ||
| return createComponent(Portal, { | ||
| mount: el, | ||
| get children() { | ||
| return createComponent(Devtools, {}); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| }), el); | ||
| return { | ||
| dispose, | ||
| eventBus | ||
| }; | ||
| } | ||
| export { mountDevtools }; |
| import { DevtoolsProvider, PiPProvider } from '../chunk/AP5L3KAF.js'; | ||
| import '../chunk/BNGI36V3.js'; | ||
| import { render, createComponent, Portal } from 'solid-js/web'; | ||
| import { lazy } from 'solid-js'; | ||
| import { ClientEventBus } from '@tanstack/devtools-event-bus/client'; | ||
| function mountDevtools(options) { | ||
| const { | ||
| el, | ||
| plugins, | ||
| config, | ||
| eventBusConfig, | ||
| onSetPlugins | ||
| } = options; | ||
| const eventBus = new ClientEventBus(eventBusConfig); | ||
| eventBus.start(); | ||
| const Devtools = lazy(() => import('../devtools/54BPKEIS.js')); | ||
| const dispose = render(() => createComponent(DevtoolsProvider, { | ||
| plugins, | ||
| config, | ||
| onSetPlugins, | ||
| get children() { | ||
| return createComponent(PiPProvider, { | ||
| get children() { | ||
| return createComponent(Portal, { | ||
| mount: el, | ||
| get children() { | ||
| return createComponent(Devtools, {}); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| }), el); | ||
| return { | ||
| dispose, | ||
| eventBus | ||
| }; | ||
| } | ||
| export { mountDevtools }; |
| import { Show, createSignal } from 'solid-js' | ||
| import { MainPanel } from '@tanstack/devtools-ui' | ||
| import { useStyles } from '../../styles/use-styles' | ||
| import { SocialPreviewsSection } from './social-previews' | ||
| import { SerpPreviewSection } from './serp-preview' | ||
| type SeoSubView = 'social-previews' | 'serp-preview' | ||
| export const SeoTab = () => { | ||
| const [activeView, setActiveView] = | ||
| createSignal<SeoSubView>('social-previews') | ||
| const styles = useStyles() | ||
| return ( | ||
| <MainPanel withPadding> | ||
| <nav class={styles().seoSubNav} aria-label="SEO sections"> | ||
| <button | ||
| type="button" | ||
| class={`${styles().seoSubNavLabel} ${activeView() === 'social-previews' ? styles().seoSubNavLabelActive : ''}`} | ||
| onClick={() => setActiveView('social-previews')} | ||
| > | ||
| Social previews | ||
| </button> | ||
| <button | ||
| type="button" | ||
| class={`${styles().seoSubNavLabel} ${activeView() === 'serp-preview' ? styles().seoSubNavLabelActive : ''}`} | ||
| onClick={() => setActiveView('serp-preview')} | ||
| > | ||
| SERP Preview | ||
| </button> | ||
| </nav> | ||
| <Show when={activeView() === 'social-previews'}> | ||
| <SocialPreviewsSection /> | ||
| </Show> | ||
| <Show when={activeView() === 'serp-preview'}> | ||
| <SerpPreviewSection /> | ||
| </Show> | ||
| </MainPanel> | ||
| ) | ||
| } |
| import { Section, SectionDescription } from '@tanstack/devtools-ui' | ||
| import { For, createMemo, createSignal } from 'solid-js' | ||
| import { useHeadChanges } from '../../hooks/use-head-changes' | ||
| import { useStyles } from '../../styles/use-styles' | ||
| /** Google typically truncates titles at ~60 characters. */ | ||
| const TITLE_MAX_CHARS = 60 | ||
| /** Meta description is often trimmed at ~158 characters on desktop. */ | ||
| const DESCRIPTION_MAX_CHARS = 158 | ||
| /** Approximate characters that fit in 3 lines at mobile width (~340px, ~14px font). */ | ||
| const DESCRIPTION_MOBILE_MAX_CHARS = 120 | ||
| const ELLIPSIS = '...' | ||
| type SerpData = { | ||
| title: string | ||
| description: string | ||
| siteName: string | ||
| favicon: string | null | ||
| url: string | ||
| } | ||
| type SerpOverflow = { | ||
| titleOverflow: boolean | ||
| descriptionOverflow: boolean | ||
| descriptionOverflowMobile: boolean | ||
| } | ||
| type SerpCheck = { | ||
| message: string | ||
| hasIssue: (data: SerpData, overflow: SerpOverflow) => boolean | ||
| } | ||
| type SerpPreview = { | ||
| label: string | ||
| isMobile: boolean | ||
| extraChecks: Array<SerpCheck> | ||
| } | ||
| const COMMON_CHECKS: Array<SerpCheck> = [ | ||
| { | ||
| message: 'No favicon or icon set on the page.', | ||
| hasIssue: (data) => !data.favicon, | ||
| }, | ||
| { | ||
| message: 'No title tag set on the page.', | ||
| hasIssue: (data) => !data.title.trim(), | ||
| }, | ||
| { | ||
| message: 'No meta description set on the page.', | ||
| hasIssue: (data) => !data.description.trim(), | ||
| }, | ||
| { | ||
| message: | ||
| 'The title is wider than 600px and it may not be displayed in full length.', | ||
| hasIssue: (_, overflow) => overflow.titleOverflow, | ||
| }, | ||
| ] | ||
| const SERP_PREVIEWS: Array<SerpPreview> = [ | ||
| { | ||
| label: 'Desktop preview', | ||
| isMobile: false, | ||
| extraChecks: [ | ||
| { | ||
| message: | ||
| 'The meta description may get trimmed at ~960 pixels on desktop and at ~680px on mobile. Keep it below ~158 characters.', | ||
| hasIssue: (_, overflow) => overflow.descriptionOverflow, | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| label: 'Mobile preview', | ||
| isMobile: true, | ||
| extraChecks: [ | ||
| { | ||
| message: | ||
| 'Description exceeds the 3-line limit for mobile view. Please shorten your text to fit within 3 lines.', | ||
| hasIssue: (_, overflow) => overflow.descriptionOverflowMobile, | ||
| }, | ||
| ], | ||
| }, | ||
| ] | ||
| function truncateToChars(text: string, maxChars: number): string { | ||
| if (text.length <= maxChars) return text | ||
| if (maxChars <= ELLIPSIS.length) return ELLIPSIS | ||
| return text.slice(0, maxChars - ELLIPSIS.length) + ELLIPSIS | ||
| } | ||
| function getSerpFromHead(): SerpData { | ||
| const title = document.title || '' | ||
| const url = typeof window !== 'undefined' ? window.location.href : '' | ||
| const metaTags = Array.from(document.head.querySelectorAll('meta')) | ||
| const descriptionMeta = metaTags.find( | ||
| (m) => m.getAttribute('name')?.toLowerCase() === 'description', | ||
| ) | ||
| const description = descriptionMeta?.getAttribute('content')?.trim() || '' | ||
| const siteNameMeta = metaTags.find( | ||
| (m) => m.getAttribute('property') === 'og:site_name', | ||
| ) | ||
| const siteName = | ||
| siteNameMeta?.getAttribute('content')?.trim() || | ||
| (typeof window !== 'undefined' | ||
| ? window.location.hostname.replace(/^www\./, '') | ||
| : '') | ||
| const linkTags = Array.from(document.head.querySelectorAll('link')) | ||
| const iconLink = linkTags.find((l) => | ||
| l.getAttribute('rel')?.toLowerCase().split(/\s+/).includes('icon'), | ||
| ) | ||
| let favicon: string | null = iconLink?.getAttribute('href') || null | ||
| if (favicon && typeof window !== 'undefined') { | ||
| try { | ||
| favicon = new URL(favicon, url).href | ||
| } catch { | ||
| favicon = null | ||
| } | ||
| } | ||
| return { title, description, siteName, favicon, url } | ||
| } | ||
| function getSerpIssues( | ||
| data: SerpData, | ||
| overflow: SerpOverflow, | ||
| checks: Array<SerpCheck>, | ||
| ): Array<string> { | ||
| return checks.filter((c) => c.hasIssue(data, overflow)).map((c) => c.message) | ||
| } | ||
| function SerpSnippetPreview(props: { | ||
| data: SerpData | ||
| displayTitle: string | ||
| displayDescription: string | ||
| isMobile: boolean | ||
| label: string | ||
| issues: Array<string> | ||
| }) { | ||
| const styles = useStyles() | ||
| return ( | ||
| <div class={styles().serpPreviewBlock}> | ||
| <div class={styles().serpPreviewLabel}>{props.label}</div> | ||
| <div | ||
| class={ | ||
| props.isMobile ? styles().serpSnippetMobile : styles().serpSnippet | ||
| } | ||
| > | ||
| <div class={styles().serpSnippetTopRow}> | ||
| {props.data.favicon ? ( | ||
| <img | ||
| src={props.data.favicon} | ||
| alt="favicon icon" | ||
| class={styles().serpSnippetFavicon} | ||
| /> | ||
| ) : ( | ||
| <div class={styles().serpSnippetDefaultFavicon} /> | ||
| )} | ||
| <div class={styles().serpSnippetSiteColumn}> | ||
| <span class={styles().serpSnippetSiteName}> | ||
| {props.data.siteName || props.data.url} | ||
| </span> | ||
| <span class={styles().serpSnippetSiteUrl}>{props.data.url}</span> | ||
| </div> | ||
| </div> | ||
| <div class={styles().serpSnippetTitle}> | ||
| {props.displayTitle || props.data.title || 'No title'} | ||
| </div> | ||
| {!props.isMobile && ( | ||
| <div class={styles().serpSnippetDesc}> | ||
| {props.displayDescription || | ||
| props.data.description || | ||
| 'No meta description.'} | ||
| </div> | ||
| )} | ||
| {props.isMobile && ( | ||
| <div class={styles().serpSnippetDescMobile}> | ||
| {props.displayDescription || | ||
| props.data.description || | ||
| 'No meta description.'} | ||
| </div> | ||
| )} | ||
| </div> | ||
| {props.issues.length > 0 ? ( | ||
| <div class={styles().seoMissingTagsSection}> | ||
| <strong>Issues for {props.label}:</strong> | ||
| <ul class={styles().serpErrorList}> | ||
| <For each={props.issues}> | ||
| {(issue) => <li class={styles().serpReportItem}>{issue}</li>} | ||
| </For> | ||
| </ul> | ||
| </div> | ||
| ) : null} | ||
| </div> | ||
| ) | ||
| } | ||
| export function SerpPreviewSection() { | ||
| const [serp, setSerp] = createSignal<SerpData>(getSerpFromHead()) | ||
| useHeadChanges(() => { | ||
| setSerp(getSerpFromHead()) | ||
| }) | ||
| const serpPreviewState = createMemo(() => { | ||
| const data = serp() | ||
| const titleText = data.title || 'No title' | ||
| const descText = data.description || 'No meta description.' | ||
| const displayTitle = truncateToChars(titleText, TITLE_MAX_CHARS) | ||
| const displayDescription = truncateToChars(descText, DESCRIPTION_MAX_CHARS) | ||
| return { | ||
| displayTitle, | ||
| displayDescription, | ||
| overflow: { | ||
| titleOverflow: titleText.length > TITLE_MAX_CHARS, | ||
| descriptionOverflow: descText.length > DESCRIPTION_MAX_CHARS, | ||
| descriptionOverflowMobile: | ||
| descText.length > DESCRIPTION_MOBILE_MAX_CHARS, | ||
| }, | ||
| } | ||
| }) | ||
| return ( | ||
| <Section> | ||
| <SectionDescription> | ||
| See how your title tag and meta description may look in Google search | ||
| results. Data is read from the current page. | ||
| </SectionDescription> | ||
| <For each={SERP_PREVIEWS}> | ||
| {(preview) => { | ||
| const issues = createMemo(() => | ||
| getSerpIssues(serp(), serpPreviewState().overflow, [ | ||
| ...COMMON_CHECKS, | ||
| ...preview.extraChecks, | ||
| ]), | ||
| ) | ||
| return ( | ||
| <SerpSnippetPreview | ||
| data={serp()} | ||
| displayTitle={serpPreviewState().displayTitle} | ||
| displayDescription={serpPreviewState().displayDescription} | ||
| isMobile={preview.isMobile} | ||
| label={preview.label} | ||
| issues={issues()} | ||
| /> | ||
| ) | ||
| }} | ||
| </For> | ||
| </Section> | ||
| ) | ||
| } |
| import { For, createSignal } from 'solid-js' | ||
| import { Section, SectionDescription } from '@tanstack/devtools-ui' | ||
| import { useStyles } from '../../styles/use-styles' | ||
| import { useHeadChanges } from '../../hooks/use-head-changes' | ||
| const SOCIALS = [ | ||
| { | ||
| network: 'Facebook', | ||
| tags: [ | ||
| { key: 'og:title', prop: 'title' }, | ||
| { key: 'og:description', prop: 'description' }, | ||
| { key: 'og:image', prop: 'image' }, | ||
| { key: 'og:url', prop: 'url' }, | ||
| ], | ||
| color: '#4267B2', | ||
| }, | ||
| { | ||
| network: 'X/Twitter', | ||
| tags: [ | ||
| { key: 'twitter:title', prop: 'title' }, | ||
| { key: 'twitter:description', prop: 'description' }, | ||
| { key: 'twitter:image', prop: 'image' }, | ||
| { key: 'twitter:url', prop: 'url' }, | ||
| ], | ||
| color: '#1DA1F2', | ||
| }, | ||
| { | ||
| network: 'LinkedIn', | ||
| tags: [ | ||
| { key: 'og:title', prop: 'title' }, | ||
| { key: 'og:description', prop: 'description' }, | ||
| { key: 'og:image', prop: 'image' }, | ||
| { key: 'og:url', prop: 'url' }, | ||
| ], | ||
| color: '#0077B5', | ||
| }, | ||
| { | ||
| network: 'Discord', | ||
| tags: [ | ||
| { key: 'og:title', prop: 'title' }, | ||
| { key: 'og:description', prop: 'description' }, | ||
| { key: 'og:image', prop: 'image' }, | ||
| { key: 'og:url', prop: 'url' }, | ||
| ], | ||
| color: '#5865F2', | ||
| }, | ||
| { | ||
| network: 'Slack', | ||
| tags: [ | ||
| { key: 'og:title', prop: 'title' }, | ||
| { key: 'og:description', prop: 'description' }, | ||
| { key: 'og:image', prop: 'image' }, | ||
| { key: 'og:url', prop: 'url' }, | ||
| ], | ||
| color: '#4A154B', | ||
| }, | ||
| { | ||
| network: 'Mastodon', | ||
| tags: [ | ||
| { key: 'og:title', prop: 'title' }, | ||
| { key: 'og:description', prop: 'description' }, | ||
| { key: 'og:image', prop: 'image' }, | ||
| { key: 'og:url', prop: 'url' }, | ||
| ], | ||
| color: '#6364FF', | ||
| }, | ||
| { | ||
| network: 'Bluesky', | ||
| tags: [ | ||
| { key: 'og:title', prop: 'title' }, | ||
| { key: 'og:description', prop: 'description' }, | ||
| { key: 'og:image', prop: 'image' }, | ||
| { key: 'og:url', prop: 'url' }, | ||
| ], | ||
| color: '#1185FE', | ||
| }, | ||
| // Add more networks as needed | ||
| ] | ||
| type SocialMeta = { | ||
| title?: string | ||
| description?: string | ||
| image?: string | ||
| url?: string | ||
| } | ||
| type SocialReport = { | ||
| network: string | ||
| found: Partial<SocialMeta> | ||
| missing: Array<string> | ||
| } | ||
| function SocialPreview(props: { | ||
| meta: SocialMeta | ||
| color: string | ||
| network: string | ||
| }) { | ||
| const styles = useStyles() | ||
| return ( | ||
| <div | ||
| class={styles().seoPreviewCard} | ||
| style={{ 'border-color': props.color }} | ||
| > | ||
| <div class={styles().seoPreviewHeader} style={{ color: props.color }}> | ||
| {props.network} Preview | ||
| </div> | ||
| {props.meta.image ? ( | ||
| <img | ||
| src={props.meta.image} | ||
| alt="Preview" | ||
| class={styles().seoPreviewImage} | ||
| /> | ||
| ) : ( | ||
| <div | ||
| class={styles().seoPreviewImage} | ||
| style={{ | ||
| background: '#222', | ||
| color: '#888', | ||
| display: 'flex', | ||
| 'align-items': 'center', | ||
| 'justify-content': 'center', | ||
| 'min-height': '80px', | ||
| width: '100%', | ||
| }} | ||
| > | ||
| No Image | ||
| </div> | ||
| )} | ||
| <div class={styles().seoPreviewTitle}> | ||
| {props.meta.title || 'No Title'} | ||
| </div> | ||
| <div class={styles().seoPreviewDesc}> | ||
| {props.meta.description || 'No Description'} | ||
| </div> | ||
| <div class={styles().seoPreviewUrl}> | ||
| {props.meta.url || window.location.href} | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
| export function SocialPreviewsSection() { | ||
| const [reports, setReports] = createSignal<Array<SocialReport>>(analyzeHead()) | ||
| const styles = useStyles() | ||
| function analyzeHead(): Array<SocialReport> { | ||
| const metaTags = Array.from(document.head.querySelectorAll('meta')) | ||
| const reports: Array<SocialReport> = [] | ||
| for (const social of SOCIALS) { | ||
| const found: Partial<SocialMeta> = {} | ||
| const missing: Array<string> = [] | ||
| for (const tag of social.tags) { | ||
| const meta = metaTags.find( | ||
| (m) => | ||
| (tag.key.includes('twitter:') | ||
| ? false | ||
| : m.getAttribute('property') === tag.key) || | ||
| m.getAttribute('name') === tag.key, | ||
| ) | ||
| if (meta && meta.getAttribute('content')) { | ||
| found[tag.prop as keyof SocialMeta] = | ||
| meta.getAttribute('content') || undefined | ||
| } else { | ||
| missing.push(tag.key) | ||
| } | ||
| } | ||
| reports.push({ network: social.network, found, missing }) | ||
| } | ||
| return reports | ||
| } | ||
| useHeadChanges(() => { | ||
| setReports(analyzeHead()) | ||
| }) | ||
| return ( | ||
| <Section> | ||
| <SectionDescription> | ||
| See how your current page will look when shared on popular social | ||
| networks. The tool checks for essential meta tags and highlights any | ||
| that are missing. | ||
| </SectionDescription> | ||
| <div class={styles().seoPreviewSection}> | ||
| <For each={reports()}> | ||
| {(report, i) => { | ||
| const social = SOCIALS[i()] | ||
| return ( | ||
| <div> | ||
| <SocialPreview | ||
| meta={report.found} | ||
| color={social!.color} | ||
| network={social!.network} | ||
| /> | ||
| {report.missing.length > 0 ? ( | ||
| <> | ||
| <div class={styles().seoMissingTagsSection}> | ||
| <strong>Missing tags for {social?.network}:</strong> | ||
| <ul class={styles().seoMissingTagsList}> | ||
| <For each={report.missing}> | ||
| {(tag) => ( | ||
| <li class={styles().seoMissingTag}>{tag}</li> | ||
| )} | ||
| </For> | ||
| </ul> | ||
| </div> | ||
| </> | ||
| ) : null} | ||
| </div> | ||
| ) | ||
| }} | ||
| </For> | ||
| </div> | ||
| </Section> | ||
| ) | ||
| } |
+1
-1
@@ -32,3 +32,3 @@ export { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID } from './chunk/A767CXXU.js'; | ||
| this.#abortMount = false; | ||
| import('./mount-impl/YYPMJI4G.js').then(({ mountDevtools }) => { | ||
| import('./mount-impl/3OW54XWT.js').then(({ mountDevtools }) => { | ||
| if (this.#abortMount) { | ||
@@ -35,0 +35,0 @@ this.#isMounting = false; |
+1
-1
@@ -32,3 +32,3 @@ export { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID } from './chunk/A767CXXU.js'; | ||
| this.#abortMount = false; | ||
| import('./mount-impl/YYPMJI4G.js').then(({ mountDevtools }) => { | ||
| import('./mount-impl/3OW54XWT.js').then(({ mountDevtools }) => { | ||
| if (this.#abortMount) { | ||
@@ -35,0 +35,0 @@ this.#isMounting = false; |
+1
-1
@@ -32,3 +32,3 @@ export { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID } from './chunk/A767CXXU.js'; | ||
| this.#abortMount = false; | ||
| import('./mount-impl/IGHTIX6Y.js').then(({ mountDevtools }) => { | ||
| import('./mount-impl/QJF6LBVF.js').then(({ mountDevtools }) => { | ||
| if (this.#abortMount) { | ||
@@ -35,0 +35,0 @@ this.#isMounting = false; |
+1
-1
| { | ||
| "name": "@tanstack/devtools", | ||
| "version": "0.11.0", | ||
| "version": "0.11.1", | ||
| "description": "TanStack Devtools is a set of tools for building advanced devtools for your application.", | ||
@@ -5,0 +5,0 @@ "author": "Tanner Linsley", |
+165
-0
@@ -122,2 +122,28 @@ import * as goober from 'goober' | ||
| `, | ||
| seoSubNav: css` | ||
| display: flex; | ||
| flex-direction: row; | ||
| gap: 0; | ||
| margin-bottom: 1rem; | ||
| border-bottom: 1px solid ${t(colors.gray[200], colors.gray[800])}; | ||
| `, | ||
| seoSubNavLabel: css` | ||
| padding: 0.5rem 1rem; | ||
| font-size: 0.875rem; | ||
| font-weight: 500; | ||
| color: ${t(colors.gray[600], colors.gray[400])}; | ||
| background: none; | ||
| border: none; | ||
| border-bottom: 2px solid transparent; | ||
| margin-bottom: -1px; | ||
| cursor: pointer; | ||
| font-family: inherit; | ||
| &:hover { | ||
| color: ${t(colors.gray[800], colors.gray[200])}; | ||
| } | ||
| `, | ||
| seoSubNavLabelActive: css` | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| border-bottom-color: ${t(colors.gray[900], colors.gray[100])}; | ||
| `, | ||
| seoPreviewSection: css` | ||
@@ -209,2 +235,135 @@ display: flex; | ||
| `, | ||
| serpPreviewBlock: css` | ||
| margin-bottom: 1.5rem; | ||
| border: 1px solid ${t(colors.gray[200], colors.gray[700])}; | ||
| border-radius: 10px; | ||
| padding: 1rem; | ||
| `, | ||
| serpPreviewLabel: css` | ||
| font-size: 0.875rem; | ||
| font-weight: 600; | ||
| margin-bottom: 0.5rem; | ||
| color: ${t(colors.gray[700], colors.gray[300])}; | ||
| `, | ||
| serpSnippet: css` | ||
| border: 1px solid ${t(colors.gray[100], colors.gray[800])}; | ||
| border-radius: 8px; | ||
| padding: 1rem 1.25rem; | ||
| background: ${t(colors.white, colors.darkGray[900])}; | ||
| max-width: 600px; | ||
| font-family: ${fontFamily.sans}; | ||
| box-shadow: 0 1px 2px ${t('rgba(0,0,0,0.04)', 'rgba(0,0,0,0.08)')}; | ||
| `, | ||
| serpSnippetMobile: css` | ||
| border: 1px solid ${t(colors.gray[100], colors.gray[800])}; | ||
| border-radius: 8px; | ||
| padding: 1rem 1.25rem; | ||
| background: ${t(colors.white, colors.darkGray[900])}; | ||
| max-width: 380px; | ||
| font-family: ${fontFamily.sans}; | ||
| box-shadow: 0 1px 2px ${t('rgba(0,0,0,0.04)', 'rgba(0,0,0,0.08)')}; | ||
| `, | ||
| serpSnippetDescMobile: css` | ||
| font-size: 0.875rem; | ||
| color: ${t(colors.gray[700], colors.gray[300])}; | ||
| margin: 0; | ||
| line-height: 1.5; | ||
| display: -webkit-box; | ||
| -webkit-box-orient: vertical; | ||
| -webkit-line-clamp: 3; | ||
| overflow: hidden; | ||
| `, | ||
| serpSnippetTopRow: css` | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 12px; | ||
| margin-bottom: 8px; | ||
| `, | ||
| serpSnippetFavicon: css` | ||
| width: 28px; | ||
| height: 28px; | ||
| border-radius: 50%; | ||
| flex-shrink: 0; | ||
| object-fit: contain; | ||
| overflow: hidden; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| `, | ||
| serpSnippetDefaultFavicon: css` | ||
| width: 28px; | ||
| height: 28px; | ||
| background-color: ${t(colors.gray[200], colors.gray[800])}; | ||
| border-radius: 50%; | ||
| flex-shrink: 0; | ||
| object-fit: contain; | ||
| overflow: hidden; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| `, | ||
| serpSnippetSiteColumn: css` | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 0; | ||
| min-width: 0; | ||
| `, | ||
| serpSnippetSiteName: css` | ||
| font-size: 0.875rem; | ||
| color: ${t(colors.gray[900], colors.gray[100])}; | ||
| line-height: 1.4; | ||
| margin: 0; | ||
| `, | ||
| serpSnippetSiteUrl: css` | ||
| font-size: 0.75rem; | ||
| color: ${t(colors.gray[500], colors.gray[500])}; | ||
| line-height: 1.4; | ||
| margin: 0; | ||
| `, | ||
| serpSnippetTitle: css` | ||
| font-size: 1.25rem; | ||
| font-weight: 400; | ||
| color: ${t('#1a0dab', '#8ab4f8')}; | ||
| margin: 0 0 4px 0; | ||
| line-height: 1.3; | ||
| `, | ||
| serpSnippetDesc: css` | ||
| font-size: 0.875rem; | ||
| color: ${t(colors.gray[700], colors.gray[300])}; | ||
| margin: 0; | ||
| line-height: 1.5; | ||
| `, | ||
| serpMeasureHidden: css` | ||
| position: absolute; | ||
| left: -9999px; | ||
| top: 0; | ||
| visibility: hidden; | ||
| pointer-events: none; | ||
| box-sizing: border-box; | ||
| `, | ||
| serpMeasureHiddenMobile: css` | ||
| position: absolute; | ||
| left: -9999px; | ||
| top: 0; | ||
| width: 340px; | ||
| visibility: hidden; | ||
| pointer-events: none; | ||
| font-size: 0.875rem; | ||
| line-height: 1.5; | ||
| `, | ||
| serpReportSection: css` | ||
| margin-top: 1rem; | ||
| font-size: 0.875rem; | ||
| color: ${t(colors.gray[700], colors.gray[300])}; | ||
| `, | ||
| serpErrorList: css` | ||
| margin: 4px 0 0 0; | ||
| padding-left: 1.25rem; | ||
| list-style-type: disc; | ||
| `, | ||
| serpReportItem: css` | ||
| margin-top: 0.25rem; | ||
| color: ${t(colors.red[700], colors.red[400])}; | ||
| font-size: 0.875rem; | ||
| `, | ||
| devtoolsPanelContainer: ( | ||
@@ -511,2 +670,8 @@ panelLocation: TanStackDevtoolsConfig['panelLocation'], | ||
| & > * > * { | ||
| min-width: 0; | ||
| min-height: 0; | ||
| height: 100%; | ||
| } | ||
| &:not(:last-child) { | ||
@@ -513,0 +678,0 @@ border-right: 5px solid ${t(colors.purple[200], colors.purple[800])}; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import { DevtoolsProvider, PiPProvider } from '../chunk/AP5L3KAF.js'; | ||
| import '../chunk/BNGI36V3.js'; | ||
| import { render, createComponent, Portal } from 'solid-js/web'; | ||
| import { lazy } from 'solid-js'; | ||
| import { ClientEventBus } from '@tanstack/devtools-event-bus/client'; | ||
| function mountDevtools(options) { | ||
| const { | ||
| el, | ||
| plugins, | ||
| config, | ||
| eventBusConfig, | ||
| onSetPlugins | ||
| } = options; | ||
| const eventBus = new ClientEventBus(eventBusConfig); | ||
| eventBus.start(); | ||
| const Devtools = lazy(() => import('../devtools/UZDPUP5E.js')); | ||
| const dispose = render(() => createComponent(DevtoolsProvider, { | ||
| plugins, | ||
| config, | ||
| onSetPlugins, | ||
| get children() { | ||
| return createComponent(PiPProvider, { | ||
| get children() { | ||
| return createComponent(Portal, { | ||
| mount: el, | ||
| get children() { | ||
| return createComponent(Devtools, {}); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| }), el); | ||
| return { | ||
| dispose, | ||
| eventBus | ||
| }; | ||
| } | ||
| export { mountDevtools }; |
| import { DevtoolsProvider, PiPProvider } from '../chunk/AP5L3KAF.js'; | ||
| import '../chunk/BNGI36V3.js'; | ||
| import { render, createComponent, Portal } from 'solid-js/web'; | ||
| import { lazy } from 'solid-js'; | ||
| import { ClientEventBus } from '@tanstack/devtools-event-bus/client'; | ||
| function mountDevtools(options) { | ||
| const { | ||
| el, | ||
| plugins, | ||
| config, | ||
| eventBusConfig, | ||
| onSetPlugins | ||
| } = options; | ||
| const eventBus = new ClientEventBus(eventBusConfig); | ||
| eventBus.start(); | ||
| const Devtools = lazy(() => import('../devtools/AKRRB3KC.js')); | ||
| const dispose = render(() => createComponent(DevtoolsProvider, { | ||
| plugins, | ||
| config, | ||
| onSetPlugins, | ||
| get children() { | ||
| return createComponent(PiPProvider, { | ||
| get children() { | ||
| return createComponent(Portal, { | ||
| mount: el, | ||
| get children() { | ||
| return createComponent(Devtools, {}); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| }), el); | ||
| return { | ||
| dispose, | ||
| eventBus | ||
| }; | ||
| } | ||
| export { mountDevtools }; |
| import { For, createSignal } from 'solid-js' | ||
| import { | ||
| MainPanel, | ||
| Section, | ||
| SectionDescription, | ||
| SectionIcon, | ||
| SectionTitle, | ||
| } from '@tanstack/devtools-ui' | ||
| import { SocialBubble } from '@tanstack/devtools-ui/icons' | ||
| import { useStyles } from '../styles/use-styles' | ||
| import { useHeadChanges } from '../hooks/use-head-changes' | ||
| type SocialMeta = { | ||
| title?: string | ||
| description?: string | ||
| image?: string | ||
| url?: string | ||
| } | ||
| type SocialReport = { | ||
| network: string | ||
| found: Partial<SocialMeta> | ||
| missing: Array<string> | ||
| } | ||
| const SOCIALS = [ | ||
| { | ||
| network: 'Facebook', | ||
| tags: [ | ||
| { key: 'og:title', prop: 'title' }, | ||
| { key: 'og:description', prop: 'description' }, | ||
| { key: 'og:image', prop: 'image' }, | ||
| { key: 'og:url', prop: 'url' }, | ||
| ], | ||
| color: '#4267B2', | ||
| }, | ||
| { | ||
| network: 'X/Twitter', | ||
| tags: [ | ||
| { key: 'twitter:title', prop: 'title' }, | ||
| { key: 'twitter:description', prop: 'description' }, | ||
| { key: 'twitter:image', prop: 'image' }, | ||
| { key: 'twitter:url', prop: 'url' }, | ||
| ], | ||
| color: '#1DA1F2', | ||
| }, | ||
| { | ||
| network: 'LinkedIn', | ||
| tags: [ | ||
| { key: 'og:title', prop: 'title' }, | ||
| { key: 'og:description', prop: 'description' }, | ||
| { key: 'og:image', prop: 'image' }, | ||
| { key: 'og:url', prop: 'url' }, | ||
| ], | ||
| color: '#0077B5', | ||
| }, | ||
| { | ||
| network: 'Discord', | ||
| tags: [ | ||
| { key: 'og:title', prop: 'title' }, | ||
| { key: 'og:description', prop: 'description' }, | ||
| { key: 'og:image', prop: 'image' }, | ||
| { key: 'og:url', prop: 'url' }, | ||
| ], | ||
| color: '#5865F2', | ||
| }, | ||
| { | ||
| network: 'Slack', | ||
| tags: [ | ||
| { key: 'og:title', prop: 'title' }, | ||
| { key: 'og:description', prop: 'description' }, | ||
| { key: 'og:image', prop: 'image' }, | ||
| { key: 'og:url', prop: 'url' }, | ||
| ], | ||
| color: '#4A154B', | ||
| }, | ||
| { | ||
| network: 'Mastodon', | ||
| tags: [ | ||
| { key: 'og:title', prop: 'title' }, | ||
| { key: 'og:description', prop: 'description' }, | ||
| { key: 'og:image', prop: 'image' }, | ||
| { key: 'og:url', prop: 'url' }, | ||
| ], | ||
| color: '#6364FF', | ||
| }, | ||
| { | ||
| network: 'Bluesky', | ||
| tags: [ | ||
| { key: 'og:title', prop: 'title' }, | ||
| { key: 'og:description', prop: 'description' }, | ||
| { key: 'og:image', prop: 'image' }, | ||
| { key: 'og:url', prop: 'url' }, | ||
| ], | ||
| color: '#1185FE', | ||
| }, | ||
| // Add more networks as needed | ||
| ] | ||
| function SocialPreview(props: { | ||
| meta: SocialMeta | ||
| color: string | ||
| network: string | ||
| }) { | ||
| const styles = useStyles() | ||
| return ( | ||
| <div | ||
| class={styles().seoPreviewCard} | ||
| style={{ 'border-color': props.color }} | ||
| > | ||
| <div class={styles().seoPreviewHeader} style={{ color: props.color }}> | ||
| {props.network} Preview | ||
| </div> | ||
| {props.meta.image ? ( | ||
| <img | ||
| src={props.meta.image} | ||
| alt="Preview" | ||
| class={styles().seoPreviewImage} | ||
| /> | ||
| ) : ( | ||
| <div | ||
| class={styles().seoPreviewImage} | ||
| style={{ | ||
| background: '#222', | ||
| color: '#888', | ||
| display: 'flex', | ||
| 'align-items': 'center', | ||
| 'justify-content': 'center', | ||
| 'min-height': '80px', | ||
| width: '100%', | ||
| }} | ||
| > | ||
| No Image | ||
| </div> | ||
| )} | ||
| <div class={styles().seoPreviewTitle}> | ||
| {props.meta.title || 'No Title'} | ||
| </div> | ||
| <div class={styles().seoPreviewDesc}> | ||
| {props.meta.description || 'No Description'} | ||
| </div> | ||
| <div class={styles().seoPreviewUrl}> | ||
| {props.meta.url || window.location.href} | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
| export const SeoTab = () => { | ||
| const [reports, setReports] = createSignal<Array<SocialReport>>(analyzeHead()) | ||
| const styles = useStyles() | ||
| function analyzeHead(): Array<SocialReport> { | ||
| const metaTags = Array.from(document.head.querySelectorAll('meta')) | ||
| const reports: Array<SocialReport> = [] | ||
| for (const social of SOCIALS) { | ||
| const found: Partial<SocialMeta> = {} | ||
| const missing: Array<string> = [] | ||
| for (const tag of social.tags) { | ||
| const meta = metaTags.find( | ||
| (m) => | ||
| (tag.key.includes('twitter:') | ||
| ? false | ||
| : m.getAttribute('property') === tag.key) || | ||
| m.getAttribute('name') === tag.key, | ||
| ) | ||
| if (meta && meta.getAttribute('content')) { | ||
| found[tag.prop as keyof SocialMeta] = | ||
| meta.getAttribute('content') || undefined | ||
| } else { | ||
| missing.push(tag.key) | ||
| } | ||
| } | ||
| reports.push({ network: social.network, found, missing }) | ||
| } | ||
| return reports | ||
| } | ||
| useHeadChanges(() => { | ||
| setReports(analyzeHead()) | ||
| }) | ||
| return ( | ||
| <MainPanel withPadding> | ||
| <Section> | ||
| <SectionTitle> | ||
| <SectionIcon> | ||
| <SocialBubble /> | ||
| </SectionIcon> | ||
| Social previews | ||
| </SectionTitle> | ||
| <SectionDescription> | ||
| See how your current page will look when shared on popular social | ||
| networks. The tool checks for essential meta tags and highlights any | ||
| that are missing. | ||
| </SectionDescription> | ||
| <div class={styles().seoPreviewSection}> | ||
| <For each={reports()}> | ||
| {(report, i) => { | ||
| const social = SOCIALS[i()] | ||
| return ( | ||
| <div> | ||
| <SocialPreview | ||
| meta={report.found} | ||
| color={social!.color} | ||
| network={social!.network} | ||
| /> | ||
| {report.missing.length > 0 ? ( | ||
| <> | ||
| <div class={styles().seoMissingTagsSection}> | ||
| <strong>Missing tags for {social?.network}:</strong> | ||
| <ul class={styles().seoMissingTagsList}> | ||
| <For each={report.missing}> | ||
| {(tag) => ( | ||
| <li class={styles().seoMissingTag}>{tag}</li> | ||
| )} | ||
| </For> | ||
| </ul> | ||
| </div> | ||
| </> | ||
| ) : null} | ||
| </div> | ||
| ) | ||
| }} | ||
| </For> | ||
| </div> | ||
| </Section> | ||
| </MainPanel> | ||
| ) | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
656188
6.38%70
2.94%16907
7.34%