mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(site): preserve casing for code identifiers in doc titles and OG images (#1347)
This commit is contained in:
@@ -15,7 +15,7 @@ import DocsLayout from '@/layouts/Docs.astro';
|
||||
import type { SupportedFramework } from '@/types/docs';
|
||||
import { SUPPORTED_FRAMEWORKS } from '@/types/docs';
|
||||
import { filterSidebar, getAdjacentGuides, getAllGuideSlugs, getSectionsForGuide } from '@/utils/docs/sidebar';
|
||||
import { getDocTitle } from '@/utils/docs/title';
|
||||
import { getDocTitle, isCodeIdentifier } from '@/utils/docs/title';
|
||||
import { createTechArticleSchema } from '@/utils/jsonLd/schemas';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
@@ -91,9 +91,6 @@ const jsonLdSchema = createTechArticleSchema({
|
||||
readingTime: remarkPluginFrontmatter.readingTimeMinutes,
|
||||
articleSection: sections.length > 0 ? sections.join(' > ') : undefined,
|
||||
});
|
||||
|
||||
// Leaving this in in case we change our minds
|
||||
// const isCamelOrPascalCase = (str: string) => /[a-z][A-Z]/.test(str);
|
||||
---
|
||||
|
||||
<DocsLayout doc={doc} framework={framework} slug={slug}>
|
||||
@@ -148,8 +145,7 @@ const jsonLdSchema = createTechArticleSchema({
|
||||
as="h1"
|
||||
class={clsx(
|
||||
"mt-0 mb-4 text-h25 @lg:text-h2",
|
||||
// leaving this in in case we change our minds
|
||||
// isCamelOrPascalCase(getDocTitle(doc, framework)) && "normal-case",
|
||||
isCodeIdentifier(getDocTitle(doc, framework)) && "normal-case",
|
||||
)}
|
||||
>
|
||||
{getDocTitle(doc, framework)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getDocTitle } from '../title';
|
||||
import { getDocTitle, isCodeIdentifier } from '../title';
|
||||
|
||||
describe('getDocTitle', () => {
|
||||
// Mock fixtures
|
||||
@@ -128,3 +128,40 @@ describe('getDocTitle', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCodeIdentifier', () => {
|
||||
it('should detect PascalCase', () => {
|
||||
expect(isCodeIdentifier('PlaybackRateButton')).toBe(true);
|
||||
expect(isCodeIdentifier('MuteButton')).toBe(true);
|
||||
expect(isCodeIdentifier('TimeDisplay')).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect camelCase', () => {
|
||||
expect(isCodeIdentifier('usePlayerContext')).toBe(true);
|
||||
expect(isCodeIdentifier('createPlayer')).toBe(true);
|
||||
expect(isCodeIdentifier('playbackRate')).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect kebab-case', () => {
|
||||
expect(isCodeIdentifier('playback-rate-button')).toBe(true);
|
||||
expect(isCodeIdentifier('mute-button')).toBe(true);
|
||||
expect(isCodeIdentifier('video-player')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match natural-language titles', () => {
|
||||
expect(isCodeIdentifier('Getting Started')).toBe(false);
|
||||
expect(isCodeIdentifier('Installation')).toBe(false);
|
||||
expect(isCodeIdentifier('Basic Concepts')).toBe(false);
|
||||
expect(isCodeIdentifier('How to Use')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not match single lowercase words', () => {
|
||||
expect(isCodeIdentifier('player')).toBe(false);
|
||||
expect(isCodeIdentifier('installation')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not match single PascalCase words', () => {
|
||||
expect(isCodeIdentifier('Player')).toBe(false);
|
||||
expect(isCodeIdentifier('Slider')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,3 +12,14 @@ import type { SupportedFramework } from '@/types/docs';
|
||||
export function getDocTitle(doc: CollectionEntry<'docs'>, framework: SupportedFramework): string {
|
||||
return doc.data.frameworkTitle?.[framework] ?? doc.data.title;
|
||||
}
|
||||
|
||||
const CAMEL_OR_PASCAL_CASE = /[a-z][A-Z]/;
|
||||
const KEBAB_CASE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
|
||||
|
||||
/**
|
||||
* Detect whether a title is a code identifier that should preserve its casing
|
||||
* instead of being uppercased. Matches PascalCase, camelCase, and kebab-case.
|
||||
*/
|
||||
export function isCodeIdentifier(str: string): boolean {
|
||||
return CAMEL_OR_PASCAL_CASE.test(str) || KEBAB_CASE.test(str);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Resvg } from '@resvg/resvg-js';
|
||||
import satori from 'satori';
|
||||
|
||||
import { isCodeIdentifier } from '@/utils/docs/title';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configuration — tune these values to adjust OG image appearance
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -121,8 +123,9 @@ export async function renderOgImage(options: { title?: string; size: OgSize }):
|
||||
const { width, height, topMargin } = SIZES[size];
|
||||
const fontData = await loadFont();
|
||||
|
||||
// Uppercase, then truncate if needed
|
||||
let displayTitle = title?.toUpperCase();
|
||||
// Uppercase (unless the title is a code identifier like PlaybackRateButton),
|
||||
// then truncate if needed
|
||||
let displayTitle = title && isCodeIdentifier(title) ? title : title?.toUpperCase();
|
||||
if (displayTitle && displayTitle.length > MAX_CHAR_LIMIT) {
|
||||
const truncated = displayTitle.slice(0, MAX_CHAR_LIMIT);
|
||||
const lastSpace = truncated.lastIndexOf(' ');
|
||||
|
||||
Reference in New Issue
Block a user