@opnpress/opnpress-cli 0.1.3 → 0.2.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/dist/build.js +172 -8
- package/dist/config.js +1 -0
- package/dist/render.js +7 -6
- package/package.json +2 -1
- package/templates/.skills/README.md +1 -0
- package/templates/.skills/setup-integrations.md +3 -1
- package/templates/.skills/site-audit.md +6 -5
- package/templates/.skills/theme-customization.md +2 -1
- package/templates/.skills/update-header-footer.md +2 -1
- package/templates/.skills/update-navigation.md +2 -1
- package/templates/site.config.yaml +1 -1
package/dist/build.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import sharp from 'sharp';
|
|
3
4
|
import { unified } from 'unified';
|
|
4
5
|
import remarkParse from 'remark-parse';
|
|
5
6
|
import remarkGfm from 'remark-gfm';
|
|
@@ -7,7 +8,7 @@ import YAML from 'yaml';
|
|
|
7
8
|
import { loadNavigationConfig, loadSiteConfig, loadThemeConfig } from './config.js';
|
|
8
9
|
import { buildPageTitle, isPublished, loadContentSources } from './content.js';
|
|
9
10
|
import { buildPageArtifact, writeRenderedPage } from './render.js';
|
|
10
|
-
import { escapeHtml, ensureDir, writeFileEnsured } from './utils.js';
|
|
11
|
+
import { escapeHtml, ensureDir, walkFiles, writeFileEnsured } from './utils.js';
|
|
11
12
|
async function removeIfExists(target) {
|
|
12
13
|
await fs.rm(target, { recursive: true, force: true });
|
|
13
14
|
}
|
|
@@ -21,6 +22,112 @@ async function copyPublicAssets(rootDir, distDir) {
|
|
|
21
22
|
}
|
|
22
23
|
await fs.cp(publicDir, distDir, { recursive: true, force: true });
|
|
23
24
|
}
|
|
25
|
+
function isStaticAssetPath(pathname) {
|
|
26
|
+
return /\.(avif|bmp|css|gif|ico|jpe?g|js|png|svg|webp|woff2?|ttf|otf|pdf)$/i.test(pathname);
|
|
27
|
+
}
|
|
28
|
+
function isMachineReadablePath(pathname) {
|
|
29
|
+
return /\.(html?|json|md|xml|txt)$/i.test(pathname);
|
|
30
|
+
}
|
|
31
|
+
function toHeadersPath(relativeFilePath) {
|
|
32
|
+
return `/${relativeFilePath.replace(/\\/g, '/')}`;
|
|
33
|
+
}
|
|
34
|
+
function normalizeAnalyticsId(value) {
|
|
35
|
+
if (!value) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const trimmed = value.trim();
|
|
39
|
+
if (!/^G-[A-Z0-9][A-Z0-9-]*$/i.test(trimmed)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return trimmed;
|
|
43
|
+
}
|
|
44
|
+
async function prepareHeaderLogoAsset(rootDir, distDir, logoPath) {
|
|
45
|
+
if (!logoPath) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(logoPath) || logoPath.startsWith('data:')) {
|
|
49
|
+
return logoPath;
|
|
50
|
+
}
|
|
51
|
+
const normalizedLogoPath = logoPath.replace(/^\/+/, '');
|
|
52
|
+
const sourceLogoPath = path.join(rootDir, 'public', normalizedLogoPath);
|
|
53
|
+
try {
|
|
54
|
+
await fs.access(sourceLogoPath);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return logoPath;
|
|
58
|
+
}
|
|
59
|
+
if (path.extname(normalizedLogoPath).toLowerCase() === '.svg') {
|
|
60
|
+
return `/${normalizedLogoPath}`;
|
|
61
|
+
}
|
|
62
|
+
const resizedLogoPath = normalizedLogoPath.replace(/\.(png|jpe?g|webp|avif|gif)$/i, '.opnpress.webp');
|
|
63
|
+
const outputPath = path.join(distDir, resizedLogoPath);
|
|
64
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
65
|
+
await sharp(sourceLogoPath)
|
|
66
|
+
.resize({ width: 72, height: 72, fit: 'inside', withoutEnlargement: true })
|
|
67
|
+
.webp({ quality: 82 })
|
|
68
|
+
.toFile(outputPath);
|
|
69
|
+
return `/${resizedLogoPath}`;
|
|
70
|
+
}
|
|
71
|
+
function extractAnchorLinks(html) {
|
|
72
|
+
const links = [];
|
|
73
|
+
const pattern = /<a\b[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
74
|
+
let match;
|
|
75
|
+
while ((match = pattern.exec(html))) {
|
|
76
|
+
const text = stripHtmlMarkup(match[2]).replace(/\s+/g, ' ').trim();
|
|
77
|
+
links.push({ href: match[1], text });
|
|
78
|
+
}
|
|
79
|
+
return links;
|
|
80
|
+
}
|
|
81
|
+
function duplicateLinkWarnings(html, currentRoute) {
|
|
82
|
+
const counts = new Map();
|
|
83
|
+
const labels = new Map();
|
|
84
|
+
for (const link of extractAnchorLinks(html)) {
|
|
85
|
+
if (!link.href || !link.text) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const key = `${link.href}|||${link.text.toLowerCase()}`;
|
|
89
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
90
|
+
labels.set(key, `${link.text} -> ${link.href}`);
|
|
91
|
+
}
|
|
92
|
+
return [...counts.entries()]
|
|
93
|
+
.filter(([, count]) => count > 1)
|
|
94
|
+
.map(([key, count]) => `Duplicate link purpose detected on ${currentRoute}: ${labels.get(key)} appears ${count} times.`);
|
|
95
|
+
}
|
|
96
|
+
function generatedRobotsTxt(siteDomain) {
|
|
97
|
+
const sitemapUrl = new URL('sitemap.xml', getSiteBaseUrl(siteDomain)).toString();
|
|
98
|
+
return `User-agent: *\nAllow: /\nSitemap: ${sitemapUrl}\n`;
|
|
99
|
+
}
|
|
100
|
+
function generatedHeaders(pageOutputs, assetOutputs) {
|
|
101
|
+
const securityHeaders = [
|
|
102
|
+
` Content-Security-Policy: default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; script-src 'self' https://www.googletagmanager.com 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data: https:; connect-src 'self' https://www.google-analytics.com https://www.googletagmanager.com; form-action 'self' https:; frame-src 'self' https:; require-trusted-types-for 'script'`,
|
|
103
|
+
' Cross-Origin-Opener-Policy: same-origin',
|
|
104
|
+
' Cross-Origin-Resource-Policy: same-site',
|
|
105
|
+
' Permissions-Policy: camera=(), geolocation=(), microphone=(), payment=()',
|
|
106
|
+
' Referrer-Policy: strict-origin-when-cross-origin',
|
|
107
|
+
' Strict-Transport-Security: max-age=31536000; includeSubDomains',
|
|
108
|
+
' X-Content-Type-Options: nosniff',
|
|
109
|
+
' X-Frame-Options: SAMEORIGIN',
|
|
110
|
+
' X-Permitted-Cross-Domain-Policies: none'
|
|
111
|
+
];
|
|
112
|
+
const lines = [];
|
|
113
|
+
for (const outputPath of [...pageOutputs, '/robots.txt']) {
|
|
114
|
+
lines.push(outputPath);
|
|
115
|
+
for (const header of securityHeaders) {
|
|
116
|
+
lines.push(header);
|
|
117
|
+
}
|
|
118
|
+
lines.push(' Cache-Control: public, max-age=0, must-revalidate');
|
|
119
|
+
lines.push('');
|
|
120
|
+
}
|
|
121
|
+
for (const outputPath of assetOutputs) {
|
|
122
|
+
lines.push(outputPath);
|
|
123
|
+
for (const header of securityHeaders) {
|
|
124
|
+
lines.push(header);
|
|
125
|
+
}
|
|
126
|
+
lines.push(' Cache-Control: public, max-age=31536000, immutable');
|
|
127
|
+
lines.push('');
|
|
128
|
+
}
|
|
129
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
130
|
+
}
|
|
24
131
|
function getSiteBaseUrl(domain) {
|
|
25
132
|
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(domain)) {
|
|
26
133
|
return domain.endsWith('/') ? domain : `${domain}/`;
|
|
@@ -33,14 +140,16 @@ function isExternalUrl(url) {
|
|
|
33
140
|
function isIgnoredLink(url) {
|
|
34
141
|
return url.startsWith('#') || url.startsWith('mailto:') || url.startsWith('tel:') || url.startsWith('javascript:');
|
|
35
142
|
}
|
|
36
|
-
function isStaticAssetPath(pathname) {
|
|
37
|
-
return /\.(png|jpe?g|gif|webp|svg|ico|css|js|json|xml|txt)$/i.test(pathname);
|
|
38
|
-
}
|
|
39
143
|
function isSourceMirrorPath(pathname) {
|
|
40
144
|
return pathname === '/source.md' || pathname === '/source.html' || pathname.endsWith('/source.md') || pathname.endsWith('/source.html');
|
|
41
145
|
}
|
|
42
146
|
function isGeneratedDocPath(pathname) {
|
|
43
|
-
return pathname === '/fullSiteContent.md' ||
|
|
147
|
+
return (pathname === '/fullSiteContent.md' ||
|
|
148
|
+
pathname === '/llms.txt' ||
|
|
149
|
+
pathname === '/shortcodes.md' ||
|
|
150
|
+
pathname.endsWith('/fullSiteContent.md') ||
|
|
151
|
+
pathname.endsWith('/llms.txt') ||
|
|
152
|
+
pathname.endsWith('/shortcodes.md'));
|
|
44
153
|
}
|
|
45
154
|
function extractLinks(html) {
|
|
46
155
|
const links = [];
|
|
@@ -466,20 +575,54 @@ export async function buildSite(rootDir = process.cwd()) {
|
|
|
466
575
|
const knownRoutes = new Set(published.map((source) => source.routePath));
|
|
467
576
|
const shortcodesDoc = await readOptionalTextFile(rootDir, '.skills/shortcodes.md');
|
|
468
577
|
const shortcodesUrl = shortcodesDoc ? 'shortcodes.md' : undefined;
|
|
578
|
+
const configuredAnalyticsId = site.integrations.analytics.id ?? site.integrations.analytics.measurementId;
|
|
579
|
+
const normalizedAnalyticsId = normalizeAnalyticsId(configuredAnalyticsId);
|
|
580
|
+
if (configuredAnalyticsId && !normalizedAnalyticsId) {
|
|
581
|
+
console.warn('Skipping analytics because the configured analytics ID is incomplete or invalid.');
|
|
582
|
+
}
|
|
583
|
+
const renderSite = normalizedAnalyticsId
|
|
584
|
+
? {
|
|
585
|
+
...site,
|
|
586
|
+
integrations: {
|
|
587
|
+
...site.integrations,
|
|
588
|
+
analytics: {
|
|
589
|
+
...site.integrations.analytics,
|
|
590
|
+
id: normalizedAnalyticsId,
|
|
591
|
+
measurementId: normalizedAnalyticsId
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
: {
|
|
596
|
+
...site,
|
|
597
|
+
integrations: {
|
|
598
|
+
...site.integrations,
|
|
599
|
+
analytics: {
|
|
600
|
+
...site.integrations.analytics,
|
|
601
|
+
id: undefined,
|
|
602
|
+
measurementId: undefined
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
await copyPublicAssets(rootDir, distDir);
|
|
607
|
+
const headerLogoUrl = await prepareHeaderLogoAsset(rootDir, distDir, site.site.logo);
|
|
469
608
|
const llmsPages = [];
|
|
470
609
|
const sitemapPages = [];
|
|
471
610
|
for (const source of published) {
|
|
472
611
|
const artifact = await buildPageArtifact({
|
|
473
612
|
source,
|
|
474
|
-
site,
|
|
613
|
+
site: renderSite,
|
|
475
614
|
theme,
|
|
476
615
|
navigation,
|
|
477
|
-
allPublishedSources: published
|
|
616
|
+
allPublishedSources: published,
|
|
617
|
+
headerLogoUrl
|
|
478
618
|
});
|
|
479
619
|
const outputPath = routeToOutputPath(distDir, source.routePath, 'index.html');
|
|
480
620
|
const jsonPath = routeToOutputPath(distDir, source.routePath, 'page.json');
|
|
481
621
|
const sourceMirrorPath = routeToOutputPath(distDir, source.routePath, artifact.sourceMirror?.path ?? 'source.md');
|
|
482
622
|
validateInternalLinks(artifact.html, source.routePath, site.site.domain, knownRoutes);
|
|
623
|
+
for (const warning of duplicateLinkWarnings(artifact.html, source.routePath)) {
|
|
624
|
+
console.warn(warning);
|
|
625
|
+
}
|
|
483
626
|
await writeRenderedPage(outputPath, artifact.html);
|
|
484
627
|
await writeFileEnsured(jsonPath, pageJsonToText(artifact.pageJson));
|
|
485
628
|
await writeFileEnsured(sourceMirrorPath, artifact.sourceMirror?.content ?? '');
|
|
@@ -497,7 +640,6 @@ export async function buildSite(rootDir = process.cwd()) {
|
|
|
497
640
|
updated: artifact.pageJson.updated
|
|
498
641
|
});
|
|
499
642
|
}
|
|
500
|
-
await copyPublicAssets(rootDir, distDir);
|
|
501
643
|
if (shortcodesDoc) {
|
|
502
644
|
await writeFileEnsured(path.join(distDir, 'shortcodes.md'), shortcodesDoc);
|
|
503
645
|
}
|
|
@@ -511,6 +653,28 @@ export async function buildSite(rootDir = process.cwd()) {
|
|
|
511
653
|
await writeFileEnsured(path.join(distDir, 'fullSiteContent.md'), fullSiteContentMarkdown);
|
|
512
654
|
await writeFileEnsured(path.join(distDir, 'llms.txt'), llmsTextFromPages(site.site.name, site.site.description, llmsPages, 'fullSiteContent.md', shortcodesUrl));
|
|
513
655
|
await writeFileEnsured(path.join(distDir, 'sitemap.xml'), sitemapXmlFromPages(site.site.domain, sitemapPages));
|
|
656
|
+
const distFiles = await walkFiles(distDir);
|
|
657
|
+
const generatedPageOutputs = new Set();
|
|
658
|
+
const generatedAssetOutputs = new Set();
|
|
659
|
+
for (const filePath of distFiles) {
|
|
660
|
+
const relativePath = toHeadersPath(path.relative(distDir, filePath));
|
|
661
|
+
if (relativePath === '/_headers' || relativePath === '/robots.txt') {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (isMachineReadablePath(filePath)) {
|
|
665
|
+
generatedPageOutputs.add(relativePath);
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (isStaticAssetPath(filePath)) {
|
|
669
|
+
generatedAssetOutputs.add(relativePath);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
await writeFileEnsured(path.join(distDir, '_headers'), generatedHeaders([...generatedPageOutputs].sort(), [...generatedAssetOutputs].sort()));
|
|
673
|
+
await writeFileEnsured(path.join(distDir, 'robots.txt'), generatedRobotsTxt(site.site.domain));
|
|
674
|
+
const robotsContents = await fs.readFile(path.join(distDir, 'robots.txt'), 'utf8');
|
|
675
|
+
if (!robotsContents.includes('Sitemap: ')) {
|
|
676
|
+
throw new Error('Generated robots.txt is missing the sitemap reference.');
|
|
677
|
+
}
|
|
514
678
|
console.log(`Built ${published.length} pages into ${distDir}`);
|
|
515
679
|
console.log(`Site: ${site.site.name}`);
|
|
516
680
|
const homePage = published.find((page) => page.routePath === '/') ?? published[0];
|
package/dist/config.js
CHANGED
|
@@ -83,6 +83,7 @@ const BookingCalendarIntegrationSchema = z.object({
|
|
|
83
83
|
});
|
|
84
84
|
const AnalyticsIntegrationSchema = z.object({
|
|
85
85
|
provider: z.enum(['google-analytics']).default('google-analytics'),
|
|
86
|
+
id: z.string().min(1).optional(),
|
|
86
87
|
measurementId: z.string().min(1).optional()
|
|
87
88
|
});
|
|
88
89
|
const MapsIntegrationSchema = z.object({
|
package/dist/render.js
CHANGED
|
@@ -658,8 +658,8 @@ function discoveryHeadMarkup(params) {
|
|
|
658
658
|
function normalizeShortcodeText(value) {
|
|
659
659
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
660
660
|
}
|
|
661
|
-
function analyticsSnippet(
|
|
662
|
-
const escapedId = escapeHtml(
|
|
661
|
+
function analyticsSnippet(analyticsId) {
|
|
662
|
+
const escapedId = escapeHtml(analyticsId);
|
|
663
663
|
return `
|
|
664
664
|
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedId}"></script>
|
|
665
665
|
<script>
|
|
@@ -1791,7 +1791,7 @@ function buildThemeCss(theme) {
|
|
|
1791
1791
|
`;
|
|
1792
1792
|
}
|
|
1793
1793
|
export async function buildPageArtifact(params) {
|
|
1794
|
-
const { source, site, theme, navigation, allPublishedSources } = params;
|
|
1794
|
+
const { source, site, theme, navigation, allPublishedSources, headerLogoUrl } = params;
|
|
1795
1795
|
const title = buildPageTitle(source, site.site.name);
|
|
1796
1796
|
const description = source.frontmatter.description ?? site.site.description ?? '';
|
|
1797
1797
|
const canonicalUrl = source.frontmatter.canonical
|
|
@@ -1840,13 +1840,14 @@ export async function buildPageArtifact(params) {
|
|
|
1840
1840
|
description,
|
|
1841
1841
|
url: canonicalUrl
|
|
1842
1842
|
};
|
|
1843
|
-
const
|
|
1844
|
-
|
|
1843
|
+
const analyticsId = site.integrations.analytics.id ?? site.integrations.analytics.measurementId;
|
|
1844
|
+
const analytics = analyticsId
|
|
1845
|
+
? analyticsSnippet(analyticsId)
|
|
1845
1846
|
: '';
|
|
1846
1847
|
const poweredBy = site.branding.poweredByOpnPress
|
|
1847
1848
|
? `<a class="nav-link" href="https://opnpress.com" rel="noopener noreferrer">Built with OpnPress</a>`
|
|
1848
1849
|
: '';
|
|
1849
|
-
const logoUrl = site.site.logo ? hrefForAsset(source.routePath, site.site.logo) : '';
|
|
1850
|
+
const logoUrl = headerLogoUrl ?? (site.site.logo ? hrefForAsset(source.routePath, site.site.logo) : '');
|
|
1850
1851
|
const faviconAsset = site.site.favicon ?? site.site.logo;
|
|
1851
1852
|
const faviconUrl = faviconAsset ? hrefForAsset(source.routePath, faviconAsset) : '';
|
|
1852
1853
|
const twitterSiteHandle = socialMetaHandle(site.socials.twitter ?? site.socials.x);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opnpress/opnpress-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"rehype-raw": "^7.0.0",
|
|
35
35
|
"rehype-slug": "^6.0.0",
|
|
36
36
|
"rehype-stringify": "^10.0.1",
|
|
37
|
+
"sharp": "^0.34.4",
|
|
37
38
|
"remark-gfm": "^4.0.1",
|
|
38
39
|
"remark-parse": "^11.0.0",
|
|
39
40
|
"remark-rehype": "^11.1.2",
|
|
@@ -4,3 +4,4 @@ This folder stores starter guidance for building an OpnPress site.
|
|
|
4
4
|
|
|
5
5
|
The files here are copied by `opnPress init`, so edit the templates in this repository to change the scaffold.
|
|
6
6
|
Key starter skills include page editing, navigation, integrations, and audits.
|
|
7
|
+
Build-enforced concerns such as `robots.txt`, headers, and logo optimization should be treated as generator behavior, not as manual content edits.
|
|
@@ -5,6 +5,7 @@ Use this skill when adding or changing integrations.
|
|
|
5
5
|
## What To Do
|
|
6
6
|
|
|
7
7
|
- define the provider in `site.config.yaml`
|
|
8
|
+
- use the public analytics ID field when tracking is enabled
|
|
8
9
|
- author the semantic block in markdown
|
|
9
10
|
- keep the content provider-agnostic
|
|
10
11
|
- prefer integration-backed blocks for shared site data
|
|
@@ -64,6 +65,7 @@ Each value can be a handle or a full URL. The renderer converts handles into pub
|
|
|
64
65
|
- `site.socials.*` for social identity links
|
|
65
66
|
- `site.branding.poweredByOpnPress` for footer attribution
|
|
66
67
|
- `site.deployment.provider` and `site.deployment.cloudflare.*` for publishing
|
|
68
|
+
- `site.integrations.analytics.id` for production-only analytics tracking
|
|
67
69
|
- `site.integrations.contactForm.*`
|
|
68
70
|
- `site.integrations.shareableLinks.*`
|
|
69
71
|
- `site.integrations.companyInfo.*`
|
|
@@ -81,4 +83,4 @@ Each value can be a handle or a full URL. The renderer converts handles into pub
|
|
|
81
83
|
- verify required config values are present
|
|
82
84
|
- verify the block renders instead of falling back to escaped code
|
|
83
85
|
- verify the resulting links stay relative when they should
|
|
84
|
-
|
|
86
|
+
- verify analytics is omitted when the ID is missing or incomplete
|
|
@@ -4,17 +4,18 @@ Use this skill when checking a site for missing pieces and recommending fixes.
|
|
|
4
4
|
|
|
5
5
|
## What To Check
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- duplicate links with the same purpose
|
|
8
8
|
- page titles and descriptions
|
|
9
9
|
- canonical and social metadata
|
|
10
|
-
-
|
|
11
|
-
-
|
|
10
|
+
- heading structure and semantic ordering
|
|
11
|
+
- sitemap coverage and reachable content
|
|
12
|
+
- footer branding and attribution
|
|
12
13
|
- integration config presence
|
|
13
|
-
-
|
|
14
|
+
- SEO gaps that affect crawl or clarity
|
|
14
15
|
|
|
15
16
|
## Output Style
|
|
16
17
|
|
|
17
18
|
- list what is missing
|
|
18
19
|
- explain the impact
|
|
19
20
|
- recommend the next fix in file terms
|
|
20
|
-
|
|
21
|
+
- separate content issues from build-enforced issues
|
|
@@ -9,9 +9,10 @@ Use this skill when changing the site theme.
|
|
|
9
9
|
- fonts
|
|
10
10
|
- sizing
|
|
11
11
|
- limited layout controls
|
|
12
|
+
- logo asset choices and displayed size
|
|
12
13
|
|
|
13
14
|
## Recommended Checks
|
|
14
15
|
|
|
15
16
|
- verify the theme still reads well on mobile
|
|
16
17
|
- verify contrast and spacing remain usable
|
|
17
|
-
|
|
18
|
+
- verify the header logo fits the intended slot cleanly
|
|
@@ -7,9 +7,10 @@ Use this skill when changing global header or footer content.
|
|
|
7
7
|
- edit the site shell, not individual pages, for global header/footer changes
|
|
8
8
|
- keep branding links intentional
|
|
9
9
|
- preserve readable navigation on mobile
|
|
10
|
+
- keep repeated destinations out of visible labels unless duplication is deliberate
|
|
10
11
|
|
|
11
12
|
## Recommended Checks
|
|
12
13
|
|
|
13
14
|
- verify the header still links home correctly
|
|
14
15
|
- verify the footer still includes required brand or attribution links
|
|
15
|
-
|
|
16
|
+
- verify the same destination is not repeated with confusing labels
|
|
@@ -8,9 +8,10 @@ Use this skill when editing site navigation.
|
|
|
8
8
|
- keep labels short and clear
|
|
9
9
|
- use relative internal paths for site pages
|
|
10
10
|
- keep external links explicit
|
|
11
|
+
- avoid repeated destinations unless the label and purpose are intentionally different
|
|
11
12
|
|
|
12
13
|
## Recommended Checks
|
|
13
14
|
|
|
14
15
|
- verify every internal navigation target exists
|
|
15
16
|
- verify footer and header nav stay consistent with the site structure
|
|
16
|
-
|
|
17
|
+
- verify duplicate destinations are intentional
|