mirror of
https://github.com/modernweb-dev/rocket.git
synced 2026-03-21 15:54:57 +00:00
Compare commits
8 Commits
@rocket/la
...
@rocket/cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f4a86b1a8 | ||
|
|
79e6f0df33 | ||
|
|
04621c3f16 | ||
|
|
1cd9508384 | ||
|
|
c8082fbac8 | ||
|
|
68e05f4d4a | ||
|
|
660f64c320 | ||
|
|
97d5fb2040 |
@@ -81,7 +81,7 @@
|
||||
"husky": "^4.3.7",
|
||||
"lint-staged": "^10.5.3",
|
||||
"mocha": "^9.1.3",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-fetch": "^3.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"onchange": "^7.1.0",
|
||||
"prettier": "^2.5.1",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# check-html-links
|
||||
|
||||
## 0.2.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 97d5fb2: Add external links validation via the flag `--validate-externals`.
|
||||
|
||||
You can/should provide an optional `--absolute-base-url` to handle urls starting with it as internal.
|
||||
|
||||
```bash
|
||||
# check external urls
|
||||
npx check-html-links _site --validate-externals
|
||||
|
||||
# check external urls but treat links like https://rocket.modern-web.dev/about/ as internal
|
||||
npx check-html-links _site --validate-externals --absolute-base-url https://rocket.modern-web.dev
|
||||
```
|
||||
|
||||
## 0.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -14,7 +14,7 @@ npm i -D check-html-links
|
||||
npx check-html-links _site
|
||||
```
|
||||
|
||||
For docs please see our homepage [https://rocket.modern-web.dev/docs/tools/check-html-links/](https://rocket.modern-web.dev/docs/tools/check-html-links/).
|
||||
For docs please see our homepage [https://rocket.modern-web.dev/tools/check-html-links/overview/](https://rocket.modern-web.dev/tools/check-html-links/overview/).
|
||||
|
||||
## Comparison
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "check-html-links",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -37,6 +37,7 @@
|
||||
"command-line-args": "^5.1.1",
|
||||
"glob": "^7.0.0",
|
||||
"minimatch": "^3.0.4",
|
||||
"node-fetch": "^3.0.0",
|
||||
"sax-wasm": "^2.0.0",
|
||||
"slash": "^4.0.0"
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import { validateFiles } from './validateFolder.js';
|
||||
import { prepareFiles, validateFiles } from './validateFolder.js';
|
||||
import { formatErrors } from './formatErrors.js';
|
||||
import { listFiles } from './listFiles.js';
|
||||
|
||||
@@ -18,7 +18,9 @@ export class CheckHtmlLinksCli {
|
||||
const mainDefinitions = [
|
||||
{ name: 'ignore-link-pattern', type: String, multiple: true },
|
||||
{ name: 'root-dir', type: String, defaultOption: true },
|
||||
{ name: 'continue-on-error', type: Boolean, defaultOption: false },
|
||||
{ name: 'continue-on-error', type: Boolean },
|
||||
{ name: 'validate-externals', type: Boolean },
|
||||
{ name: 'absolute-base-url', type: String },
|
||||
];
|
||||
const options = commandLineArgs(mainDefinitions, {
|
||||
stopAtFirstUnknown: true,
|
||||
@@ -29,6 +31,8 @@ export class CheckHtmlLinksCli {
|
||||
continueOnError: options['continue-on-error'],
|
||||
rootDir: options['root-dir'],
|
||||
ignoreLinkPatterns: options['ignore-link-pattern'],
|
||||
validateExternals: options['validate-externals'],
|
||||
absoluteBaseUrl: options['absolute-base-url'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,22 +47,47 @@ export class CheckHtmlLinksCli {
|
||||
}
|
||||
|
||||
async run() {
|
||||
const { ignoreLinkPatterns, rootDir: userRootDir } = this.options;
|
||||
const {
|
||||
ignoreLinkPatterns,
|
||||
rootDir: userRootDir,
|
||||
validateExternals,
|
||||
absoluteBaseUrl,
|
||||
} = this.options;
|
||||
const rootDir = userRootDir ? path.resolve(userRootDir) : process.cwd();
|
||||
const performanceStart = process.hrtime();
|
||||
|
||||
console.log('👀 Checking if all internal links work...');
|
||||
const files = await listFiles('**/*.html', rootDir);
|
||||
|
||||
console.log('Check HTML Links');
|
||||
|
||||
const filesOutput =
|
||||
files.length == 0
|
||||
? '🧐 No files to check. Did you select the correct folder?'
|
||||
: `🔥 Found a total of ${chalk.green.bold(files.length)} files to check!`;
|
||||
? ' 🧐 No files to check. Did you select the correct folder?'
|
||||
: ` 📖 Found ${chalk.green.bold(files.length)} files containing`;
|
||||
console.log(filesOutput);
|
||||
|
||||
const { errors, numberLinks } = await validateFiles(files, rootDir, { ignoreLinkPatterns });
|
||||
const { numberLinks, checkLocalFiles, checkExternalLinks } = await prepareFiles(
|
||||
files,
|
||||
rootDir,
|
||||
{
|
||||
ignoreLinkPatterns,
|
||||
validateExternals,
|
||||
absoluteBaseUrl,
|
||||
},
|
||||
);
|
||||
|
||||
console.log(`🔗 Found a total of ${chalk.green.bold(numberLinks)} links to validate!\n`);
|
||||
console.log(` 🔗 ${chalk.green.bold(numberLinks)} internal links`);
|
||||
if (validateExternals) {
|
||||
console.log(` 🌐 ${chalk.green.bold(checkExternalLinks.length)} external links`);
|
||||
}
|
||||
|
||||
console.log(' 👀 Checking...');
|
||||
|
||||
const { errors } = await validateFiles({
|
||||
checkLocalFiles,
|
||||
validateExternals,
|
||||
checkExternalLinks,
|
||||
});
|
||||
|
||||
const performance = process.hrtime(performanceStart);
|
||||
/** @type {string[]} */
|
||||
@@ -70,7 +99,7 @@ export class CheckHtmlLinksCli {
|
||||
referenceCount += error.usage.length;
|
||||
}
|
||||
output = [
|
||||
`❌ Found ${chalk.red.bold(
|
||||
` ❌ Found ${chalk.red.bold(
|
||||
errors.length.toString(),
|
||||
)} missing reference targets (used by ${referenceCount} links) while checking ${
|
||||
files.length
|
||||
@@ -78,7 +107,7 @@ export class CheckHtmlLinksCli {
|
||||
...formatErrors(errors)
|
||||
.split('\n')
|
||||
.map(line => ` ${line}`),
|
||||
`Checking links duration: ${performance[0]}s ${performance[1] / 1000000}ms`,
|
||||
` 🕑 Checking links duration: ${performance[0]}s ${performance[1] / 1000000}ms`,
|
||||
];
|
||||
message = output.join('\n');
|
||||
if (this.options.printOnError === true) {
|
||||
@@ -89,7 +118,7 @@ export class CheckHtmlLinksCli {
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`✅ All internal links are valid. (executed in ${performance[0]}s ${
|
||||
` ✅ All tested links are valid. (executed in ${performance[0]}s ${
|
||||
performance[1] / 1000000
|
||||
}ms)`,
|
||||
);
|
||||
|
||||
63
packages/check-html-links/src/checkLinks.js
Normal file
63
packages/check-html-links/src/checkLinks.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
/**
|
||||
* @type {Map<string,boolean>}
|
||||
*/
|
||||
const resultsMap = new Map();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {boolean} result
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const memorizeCheckup = (url, result) => {
|
||||
resultsMap.set(url, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} method
|
||||
* @returns
|
||||
*/
|
||||
const fetchWrap = async (url, method = 'GET') => {
|
||||
return Promise.race([
|
||||
fetch(url, { method })
|
||||
.then(response => response.ok)
|
||||
.catch(() => false),
|
||||
new Promise(resolve => setTimeout(resolve, 10000, false)),
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
const fetchHead = async url => fetchWrap(url, 'HEAD');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url - URL object to check
|
||||
* @returns {Promise<boolean>} true if url is alive or false if not
|
||||
*/
|
||||
const checkUrl = async url =>
|
||||
(fetchHead(url) || fetchWrap(url)).then(result => memorizeCheckup(url, result));
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} link - link string to check
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export const checkLink = async link => {
|
||||
const url = link.startsWith('//') ? `https:${link}` : link;
|
||||
return resultsMap.get(url) ?? checkUrl(url);
|
||||
};
|
||||
/**
|
||||
* Check an array of links and return an object with
|
||||
*
|
||||
* @param {string[]} links Links to check
|
||||
*/
|
||||
export const checkLinks = async links => Promise.all(links.map(checkLink));
|
||||
@@ -15,7 +15,7 @@ export function formatErrors(errors, relativeFrom = process.cwd()) {
|
||||
const filePath = path.relative(relativeFrom, error.filePath);
|
||||
if (error.onlyAnchorMissing === true) {
|
||||
output.push(
|
||||
`${number}. missing ${chalk.red.bold(
|
||||
` ${number}. missing ${chalk.red.bold(
|
||||
`id="${error.usage[0].anchor}"`,
|
||||
)} in ${chalk.cyanBright(filePath)}`,
|
||||
);
|
||||
@@ -24,7 +24,7 @@ export function formatErrors(errors, relativeFrom = process.cwd()) {
|
||||
const title =
|
||||
firstAttribute === 'src' || firstAttribute === 'srcset' ? 'file' : 'reference target';
|
||||
|
||||
output.push(`${number}. missing ${title} ${chalk.red.bold(filePath)}`);
|
||||
output.push(` ${number}. missing ${title} ${chalk.red.bold(filePath)}`);
|
||||
}
|
||||
const usageLength = error.usage.length;
|
||||
|
||||
@@ -34,11 +34,11 @@ export function formatErrors(errors, relativeFrom = process.cwd()) {
|
||||
const clickAbleLink = chalk.cyanBright(`${usagePath}:${usage.line + 1}:${usage.character}`);
|
||||
const attributeStart = chalk.gray(`${usage.attribute}="`);
|
||||
const attributeEnd = chalk.gray('"');
|
||||
output.push(` from ${clickAbleLink} via ${attributeStart}${usage.value}${attributeEnd}`);
|
||||
output.push(` from ${clickAbleLink} via ${attributeStart}${usage.value}${attributeEnd}`);
|
||||
}
|
||||
if (usageLength > 3) {
|
||||
const more = chalk.red((usageLength - 3).toString());
|
||||
output.push(` ... ${more} more references to this target`);
|
||||
output.push(` ... ${more} more references to this target`);
|
||||
}
|
||||
output.push('');
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ import fs from 'fs';
|
||||
import saxWasm from 'sax-wasm';
|
||||
import minimatch from 'minimatch';
|
||||
import { createRequire } from 'module';
|
||||
|
||||
import { listFiles } from './listFiles.js';
|
||||
import path from 'path';
|
||||
import slash from 'slash';
|
||||
import { listFiles } from './listFiles.js';
|
||||
import { checkLinks } from './checkLinks.js';
|
||||
|
||||
/** @typedef {import('../types/main').Link} Link */
|
||||
/** @typedef {import('../types/main').LocalFile} LocalFile */
|
||||
@@ -28,6 +28,9 @@ const parserIds = new SAXParser(SaxEventType.Attribute, streamOptions);
|
||||
/** @type {Error[]} */
|
||||
let checkLocalFiles = [];
|
||||
|
||||
/** @type {Error[]} */
|
||||
let checkExternalLinks = [];
|
||||
|
||||
/** @type {Error[]} */
|
||||
let errors = [];
|
||||
|
||||
@@ -151,6 +154,26 @@ function addLocalFile(filePath, anchor, usageObj) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filePath
|
||||
* @param {Usage} usageObj
|
||||
*/
|
||||
function addExternalLink(filePath, usageObj) {
|
||||
const foundIndex = checkExternalLinks.findIndex(item => {
|
||||
return item.filePath === filePath;
|
||||
});
|
||||
|
||||
if (foundIndex === -1) {
|
||||
checkExternalLinks.push({
|
||||
filePath,
|
||||
onlyAnchorMissing: false,
|
||||
usage: [usageObj],
|
||||
});
|
||||
} else {
|
||||
checkExternalLinks[foundIndex].usage.push(usageObj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} inValue
|
||||
*/
|
||||
@@ -200,11 +223,16 @@ function isNonHttpSchema(url) {
|
||||
* @param {object} options
|
||||
* @param {string} options.htmlFilePath
|
||||
* @param {string} options.rootDir
|
||||
* @param {string} options.absoluteBaseUrl
|
||||
* @param {function(string): boolean} options.ignoreUsage
|
||||
*/
|
||||
async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage }) {
|
||||
async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage, absoluteBaseUrl }) {
|
||||
for (const hrefObj of links) {
|
||||
const { value, anchor } = getValueAndAnchor(hrefObj.value);
|
||||
const { value: rawValue, anchor } = getValueAndAnchor(hrefObj.value);
|
||||
|
||||
const value = rawValue.startsWith(absoluteBaseUrl)
|
||||
? rawValue.substring(absoluteBaseUrl.length)
|
||||
: rawValue;
|
||||
|
||||
const usageObj = {
|
||||
attribute: hrefObj.attribute,
|
||||
@@ -229,8 +257,7 @@ async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage }) {
|
||||
} else if (valueFile === '' && anchor !== '') {
|
||||
addLocalFile(htmlFilePath, anchor, usageObj);
|
||||
} else if (value.startsWith('//') || value.startsWith('http')) {
|
||||
// TODO: handle external urls
|
||||
// external url - we do not handle that (yet)
|
||||
addExternalLink(htmlFilePath, usageObj);
|
||||
} else if (value.startsWith('/')) {
|
||||
const filePath = path.join(rootDir, valueFile);
|
||||
addLocalFile(filePath, anchor, usageObj);
|
||||
@@ -244,7 +271,7 @@ async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage }) {
|
||||
}
|
||||
}
|
||||
|
||||
return { checkLocalFiles: [...checkLocalFiles] };
|
||||
return { checkLocalFiles: [...checkLocalFiles], checkExternalLinks: [...checkExternalLinks] };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,17 +310,34 @@ async function validateLocalFiles(checkLocalFiles) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Error[]} checkExternalLinks
|
||||
*/
|
||||
async function validateExternalLinks(checkExternalLinks) {
|
||||
for await (const localFileObj of checkExternalLinks) {
|
||||
const links = localFileObj.usage.map(usage => usage.value);
|
||||
const results = await checkLinks(links);
|
||||
localFileObj.usage = localFileObj.usage.filter((link, index) => !results[index]);
|
||||
if (localFileObj.usage.length > 0) {
|
||||
errors.push(localFileObj);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} files
|
||||
* @param {string} rootDir
|
||||
* @param {Options} opts?
|
||||
*/
|
||||
export async function validateFiles(files, rootDir, opts) {
|
||||
export async function prepareFiles(files, rootDir, opts) {
|
||||
await parserReferences.prepareWasm(saxWasmBuffer);
|
||||
await parserIds.prepareWasm(saxWasmBuffer);
|
||||
|
||||
errors = [];
|
||||
checkLocalFiles = [];
|
||||
checkExternalLinks = [];
|
||||
idCache = new Map();
|
||||
let numberLinks = 0;
|
||||
|
||||
@@ -309,11 +353,27 @@ export async function validateFiles(files, rootDir, opts) {
|
||||
for (const htmlFilePath of files) {
|
||||
const { links } = await extractReferences(htmlFilePath);
|
||||
numberLinks += links.length;
|
||||
await resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage });
|
||||
await resolveLinks(links, {
|
||||
htmlFilePath,
|
||||
rootDir,
|
||||
ignoreUsage,
|
||||
absoluteBaseUrl: opts?.absoluteBaseUrl,
|
||||
});
|
||||
}
|
||||
await validateLocalFiles(checkLocalFiles);
|
||||
return { checkLocalFiles, checkExternalLinks, numberLinks };
|
||||
}
|
||||
|
||||
return { errors: errors, numberLinks: numberLinks };
|
||||
/**
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export async function validateFiles({ checkLocalFiles, validateExternals, checkExternalLinks }) {
|
||||
await validateLocalFiles(checkLocalFiles);
|
||||
if (validateExternals) {
|
||||
await validateExternalLinks(checkExternalLinks);
|
||||
}
|
||||
|
||||
return { errors };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,6 +383,14 @@ export async function validateFiles(files, rootDir, opts) {
|
||||
export async function validateFolder(inRootDir, opts) {
|
||||
const rootDir = path.resolve(inRootDir);
|
||||
const files = await listFiles('**/*.html', rootDir);
|
||||
const { errors } = await validateFiles(files, rootDir, opts);
|
||||
|
||||
const { checkLocalFiles, checkExternalLinks } = await prepareFiles(files, rootDir, opts);
|
||||
|
||||
const { errors } = await validateFiles({
|
||||
checkLocalFiles,
|
||||
validateExternals: opts?.validateExternals,
|
||||
checkExternalLinks,
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<!-- ignore known subsystems -->
|
||||
<a href="/docs/"></a>
|
||||
<a href="/developer/getting-started.html#js"></a>
|
||||
<a href="/developer/language-guides/"></a>
|
||||
<a href="/developer/javascript.html"></a>
|
||||
<!-- valid -->
|
||||
<a href="//rocket.modern-web.dev/"></a>
|
||||
<a href="http://rocket.modern-web.dev/"></a>
|
||||
<a href="https://rocket.modern-web.dev/"></a>
|
||||
|
||||
<!-- invalid -->
|
||||
<a href="//rocket.modern-web.dev/unexists-page/"></a>
|
||||
<a href="http://rocket.modern-web.dev/unexists-page/"></a>
|
||||
<a href="https://rocket.modern-web.dev/unexists-page/"></a>
|
||||
|
||||
@@ -5,8 +5,5 @@
|
||||
<a href="./page.html"></a>
|
||||
<a href=" ./page.html "></a>
|
||||
<a href=" /page.html "></a>
|
||||
<a href="//domain.com/something/"></a>
|
||||
<a href="http://domain.com/something/"></a>
|
||||
<a href="https://domain.com/something/"></a>
|
||||
<a href=""></a>
|
||||
<a href=":~:text=put%20your%20labels%20above%20your%20inputs">Sign-in form best practices</a>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
About
|
||||
@@ -0,0 +1,2 @@
|
||||
<a href="about.html">About</a>
|
||||
<a href="http://localhost/about.html">About Absolute</a>
|
||||
@@ -20,19 +20,19 @@ describe('formatErrors', () => {
|
||||
const result = await executeAndFormat('fixtures/test-case');
|
||||
expect(result.trim().split('\n')).to.deep.equal([
|
||||
'1. missing id="my-teams" in fixtures/test-case/price/index.html',
|
||||
' from fixtures/test-case/history/index.html:1:9 via href="/price/#my-teams"',
|
||||
' from fixtures/test-case/history/index.html:1:9 via href="/price/#my-teams"',
|
||||
'',
|
||||
'2. missing file fixtures/test-case/about/images/team.png',
|
||||
' from fixtures/test-case/about/index.html:3:10 via src="./images/team.png"',
|
||||
' 2. missing file fixtures/test-case/about/images/team.png',
|
||||
' from fixtures/test-case/about/index.html:3:10 via src="./images/team.png"',
|
||||
'',
|
||||
'3. missing reference target fixtures/test-case/aboot',
|
||||
' from fixtures/test-case/about/index.html:6:11 via href="/aboot"',
|
||||
' from fixtures/test-case/history/index.html:4:11 via href="/aboot"',
|
||||
' from fixtures/test-case/index.html:4:11 via href="/aboot"',
|
||||
' ... 2 more references to this target',
|
||||
' 3. missing reference target fixtures/test-case/aboot',
|
||||
' from fixtures/test-case/about/index.html:6:11 via href="/aboot"',
|
||||
' from fixtures/test-case/history/index.html:4:11 via href="/aboot"',
|
||||
' from fixtures/test-case/index.html:4:11 via href="/aboot"',
|
||||
' ... 2 more references to this target',
|
||||
'',
|
||||
'4. missing reference target fixtures/test-case/prce',
|
||||
' from fixtures/test-case/index.html:1:9 via href="./prce"',
|
||||
' 4. missing reference target fixtures/test-case/prce',
|
||||
' from fixtures/test-case/index.html:1:9 via href="./prce"',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,6 +66,74 @@ describe('validateFolder', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates external links', async () => {
|
||||
const { errors, cleanup } = await execute('fixtures/external-link', {
|
||||
validateExternals: true,
|
||||
});
|
||||
expect(cleanup(errors)).to.deep.equal([
|
||||
{
|
||||
filePath: 'fixtures/external-link/index.html',
|
||||
onlyAnchorMissing: false,
|
||||
usage: [
|
||||
{
|
||||
attribute: 'href',
|
||||
value: '//rocket.modern-web.dev/unexists-page/',
|
||||
file: 'fixtures/external-link/index.html',
|
||||
line: 6,
|
||||
character: 9,
|
||||
anchor: '',
|
||||
},
|
||||
{
|
||||
attribute: 'href',
|
||||
value: 'http://rocket.modern-web.dev/unexists-page/',
|
||||
file: 'fixtures/external-link/index.html',
|
||||
line: 7,
|
||||
character: 9,
|
||||
anchor: '',
|
||||
},
|
||||
{
|
||||
attribute: 'href',
|
||||
value: 'https://rocket.modern-web.dev/unexists-page/',
|
||||
file: 'fixtures/external-link/index.html',
|
||||
line: 8,
|
||||
character: 9,
|
||||
anchor: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates links with own absolute base url as internal', async () => {
|
||||
const { errors, cleanup } = await execute('fixtures/internal-own-absolute-base-path', {
|
||||
validateExternals: true,
|
||||
absoluteBaseUrl: 'http://localhost',
|
||||
});
|
||||
expect(cleanup(errors)).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('validates all full urls if there is no absoluteBaseUrl provided', async () => {
|
||||
const { errors, cleanup } = await execute('fixtures/internal-own-absolute-base-path', {
|
||||
validateExternals: true,
|
||||
});
|
||||
expect(cleanup(errors)).to.deep.equal([
|
||||
{
|
||||
filePath: 'fixtures/internal-own-absolute-base-path/index.html',
|
||||
onlyAnchorMissing: false,
|
||||
usage: [
|
||||
{
|
||||
anchor: '',
|
||||
attribute: 'href',
|
||||
character: 9,
|
||||
file: 'fixtures/internal-own-absolute-base-path/index.html',
|
||||
line: 1,
|
||||
value: 'http://localhost/about.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('groups multiple usage of the same missing file', async () => {
|
||||
const { errors, cleanup } = await execute('fixtures/internal-links-to-same-file');
|
||||
expect(cleanup(errors)).to.deep.equal([
|
||||
|
||||
8
packages/check-html-links/types/main.d.ts
vendored
8
packages/check-html-links/types/main.d.ts
vendored
@@ -26,13 +26,15 @@ export interface Error {
|
||||
usage: Usage[];
|
||||
}
|
||||
|
||||
interface Options {
|
||||
export interface Options {
|
||||
ignoreLinkPatterns: string[] | null;
|
||||
validateExternals: boolean;
|
||||
absoluteBaseUrl: string;
|
||||
}
|
||||
|
||||
export interface CheckHtmlLinksCliOptions {
|
||||
export interface CheckHtmlLinksCliOptions extends Options {
|
||||
printOnError: boolean;
|
||||
rootDir: string;
|
||||
ignoreLinkPatterns: string[] | null;
|
||||
continueOnError: boolean;
|
||||
absoluteBaseUrl: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# @rocket/cli
|
||||
|
||||
## 0.20.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 68e05f4: `rocket lint` can now validate external links.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
rocket lint --validate-externals
|
||||
```
|
||||
|
||||
- Updated dependencies [97d5fb2]
|
||||
- check-html-links@0.2.4
|
||||
|
||||
## 0.20.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rocket/cli",
|
||||
"version": "0.20.3",
|
||||
"version": "0.20.4",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -54,7 +54,7 @@
|
||||
"dependencies": {
|
||||
"@rocket/building-rollup": "^0.4.0",
|
||||
"@rocket/engine": "^0.2.7",
|
||||
"check-html-links": "^0.2.3",
|
||||
"check-html-links": "^0.2.4",
|
||||
"colorette": "^2.0.16",
|
||||
"commander": "^9.0.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
|
||||
@@ -24,6 +24,7 @@ export class RocketLint {
|
||||
.command('lint')
|
||||
.option('-i, --input-dir <path>', 'path to where to search for source files')
|
||||
.option('-b, --build-html', 'do a quick html only build and then check links')
|
||||
.option('-e, --validate-externals', 'validate external links')
|
||||
.action(async options => {
|
||||
const { cliOptions, ...lintOptions } = options;
|
||||
cli.setOptions({
|
||||
@@ -77,6 +78,7 @@ export class RocketLint {
|
||||
rootDir: folderToCheck,
|
||||
printOnError: true,
|
||||
continueOnError: false,
|
||||
absoluteBaseUrl: this.cli.options.absoluteBaseUrl,
|
||||
...userCheckHtmlLinksOptions,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @rocket/create
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 79e6f0d: Update error message to include an action to resolve a possible [digit error](https://github.com/Rich-Harris/degit/issues/313).
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rocket/create",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -152,7 +152,11 @@ export class CreateCli {
|
||||
await gitHandler.clone('./' + newFolderPath);
|
||||
cloneError = false;
|
||||
} catch (e) {
|
||||
console.log(`${red('✖')} Could not apply the template - maybe the url is wrong?`);
|
||||
console.log(
|
||||
`${red(
|
||||
'✖',
|
||||
)} Could not apply the template - maybe the url is wrong? If this problem persists, you could try clearing your .degit cache, usually located at ~/.degit`,
|
||||
);
|
||||
console.log(`${red('>')} ${e.message}`);
|
||||
const response = await prompts({
|
||||
type: 'text',
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @rocket/launch
|
||||
|
||||
## 0.21.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 660f64c: Change the default gitBranch to main
|
||||
- Updated dependencies [68e05f4]
|
||||
- @rocket/cli@0.20.4
|
||||
|
||||
## 0.21.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rocket/launch",
|
||||
"version": "0.21.2",
|
||||
"version": "0.21.3",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -46,7 +46,7 @@
|
||||
"preset"
|
||||
],
|
||||
"dependencies": {
|
||||
"@rocket/cli": "^0.20.3",
|
||||
"@rocket/cli": "^0.20.4",
|
||||
"@rocket/components": "^0.2.0",
|
||||
"@rocket/engine": "^0.2.7",
|
||||
"@webcomponents/template-shadowroot": "^0.1.0",
|
||||
|
||||
@@ -57,7 +57,7 @@ export class LayoutMain extends Layout {
|
||||
</picture>
|
||||
`,
|
||||
gitSiteUrl: 'https://github.com/modernweb-dev/rocket',
|
||||
gitBranch: 'next',
|
||||
gitBranch: 'main',
|
||||
description: 'A static site generator for modern web development',
|
||||
socialLinks: [
|
||||
{
|
||||
|
||||
@@ -3,6 +3,6 @@ export const defaultGlobalOptions = {
|
||||
logoSrc: '/icon.svg',
|
||||
logoAlt: 'Rocket Logo',
|
||||
gitSiteUrl: 'https://github.com/modernweb-dev/rocket',
|
||||
gitBranch: 'master',
|
||||
gitBranch: 'main',
|
||||
description: 'A static site generator for modern web development',
|
||||
};
|
||||
|
||||
@@ -93,4 +93,4 @@ If something is missing in the documentation or if you found some part confusing
|
||||
|
||||
## Credit
|
||||
|
||||
This getting started guide was originally based off of [astro](https://astro.build/) getting started guide.
|
||||
This getting started guide was originally based off the [astro](https://astro.build/) getting started guide.
|
||||
|
||||
@@ -44,11 +44,11 @@ Before we get started we need to engage the engines via
|
||||
npm start
|
||||
```
|
||||
|
||||
This with start rocket in development mode and you will see your site running in your browser.
|
||||
This will start rocket in development mode and you will see your site running in your browser.
|
||||
|
||||
## What is a page?
|
||||
|
||||
A page is a file that ends either with `*.rocket.js`, `*.rocket.md` or `*.rocket.html` and is located in the input directory (`site/pages` by default). Pages will make up the majority or your website.
|
||||
A page is a file that ends either with `*.rocket.js`, `*.rocket.md` or `*.rocket.html` and is located in the input directory (`site/pages` by default). Pages will make up the majority of your website.
|
||||
|
||||
The simplest way to get started is to create a file
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ e.g.
|
||||
|
||||
## Recommended Project Structure
|
||||
|
||||
Even if there is no enforce project structure it still makes sense to follow some common best practices.
|
||||
Even if there is no enforced project structure it still makes sense to follow some common best practices.
|
||||
|
||||
- `site/pages/*` - All the pages of your website (e.g. all `*.rocket.{js,md,html}` files)
|
||||
- `site/pages/about/_assets/*` - Keep assets related to pages close to the page itself (e.g. images, videos, ...)
|
||||
@@ -116,4 +116,4 @@ Rocket has complete control over how these files get processed, optimized, and b
|
||||
|
||||
For most users, the majority of your files will live inside of the `site/pages/` and `site/src/` directory so that Rocket can optimize them in your final build. By contrast, the `site/public/` directory is the place for any files to live outside of the Rocket build process.
|
||||
|
||||
If you put a file into the public folder, it will not be processed by Rocket. Instead it will be copied into the build folder untouched. This can be useful for specific file like `robots.txt` or `site.webmanifest` or sometimes for assets like images that you need in a specific location.
|
||||
If you put a file into the public folder, it will not be processed by Rocket. Instead it will be copied into the build folder untouched. This can be useful for specific files like `robots.txt` or `site.webmanifest` or sometimes for assets like images that you need in a specific location.
|
||||
|
||||
@@ -30,7 +30,7 @@ export const description = `An intro to Rocket pages, which is the actual websit
|
||||
|
||||
# Pages
|
||||
|
||||
**Pages** are a the only files that get rendered into the output folder.
|
||||
**Pages** are the only files that get rendered into the output folder.
|
||||
|
||||
3 types of pages are supported:
|
||||
|
||||
@@ -38,11 +38,11 @@ export const description = `An intro to Rocket pages, which is the actual websit
|
||||
- `*.rocket.html` - A page with HTML content.
|
||||
- `*.rocket.js` - A page with JavaScript content.
|
||||
|
||||
Feel free to choose a format for each pages that best suits its content.
|
||||
Feel free to choose a format for each page that best suits its content.
|
||||
|
||||
## File-based Routing
|
||||
|
||||
Rockets uses Pages to do something called **file-based routing.** Every file in your `pages` directory becomes a page on your site, using the file name to decide the final route.
|
||||
Rocket uses Pages to do something called **file-based routing.** Every file in your `pages` directory becomes a page on your site, using the file name to decide the final route.
|
||||
|
||||
```
|
||||
site/pages/index.rocket.md -> mysite.com/
|
||||
@@ -72,7 +72,7 @@ export default () => `
|
||||
|
||||
👆 This is the JavaScript version which all other formats automatically convert to.
|
||||
|
||||
However for you your convenience you can also write your page in HTML and it will be automatically converted into the JavaScript version above.
|
||||
However for your convenience you can also write your page in HTML and it will be automatically converted into the JavaScript version above.
|
||||
|
||||
👉 `site/pages/index.rocket.html`
|
||||
|
||||
@@ -104,8 +104,8 @@ Or you can write markdown.
|
||||
|
||||
Markdown and HTML files are automatically converted to their JavaScript equivalents.
|
||||
That is also how they get their template literal super powers.
|
||||
Those converted files only life for a fraction of a second and are deleted after the page is done rendering.
|
||||
If you would like to keep them for debugging or out of curiosity you can export the following flag to prefect the cleanup.
|
||||
Those converted files only live for a fraction of a second and are deleted after the page is done rendering.
|
||||
If you would like to keep them for debugging or out of curiosity you can export the following flag to prevent the cleanup.
|
||||
|
||||
```js
|
||||
export const keepConvertedFiles = true;
|
||||
|
||||
@@ -90,7 +90,7 @@ For that reasons you can override the layout via exporting `layout` on a specifi
|
||||
|
||||
In the above example we have 4 layouts we define in different places.
|
||||
|
||||
1. Default layout use by all pages unless overwritten in `recursive.data.js`
|
||||
1. Default layout used by all pages unless overwritten in `recursive.data.js`
|
||||
2. Explicitly specifying a layout `index.rocket.js`
|
||||
3. use `blog/local.data.js` to set a layout for all pages in `blog`
|
||||
4. use `docs/codelabs/local.data.js` to set a layout for all pages in `codelabs`
|
||||
|
||||
@@ -28,11 +28,11 @@ export const needsLoader = true;
|
||||
|
||||
# Components
|
||||
|
||||
Components in Rocket are the "just" the web standard [web components](https://developer.mozilla.org/en-US/docs/Web/Web_Components). They are used to create reusable components that can be used in any web page.
|
||||
Components in Rocket are "just" the web standard [web components](https://developer.mozilla.org/en-US/docs/Web/Web_Components). They are used to create reusable components that can be used in any web page.
|
||||
|
||||
<inline-notification>
|
||||
|
||||
Web component only live within the html body. For content within the head or a full html page please see [layouts](./50--layouts.rocket.md).
|
||||
Web components only live within the html body. For content within the head or a full html page please see [layouts](./50--layouts.rocket.md).
|
||||
|
||||
</inline-notification>
|
||||
|
||||
@@ -84,7 +84,7 @@ We can now put this code in Rocket JavaScript, Markdown or Html pages.
|
||||
<rocket-greeting>Go</rocket-greeting>
|
||||
````
|
||||
|
||||
will result a server rendered output that does not load ANY JavaScript
|
||||
will result in a server rendered output that does not load ANY JavaScript
|
||||
|
||||
```
|
||||
Hello World
|
||||
@@ -124,7 +124,7 @@ Note the empty lines between html & markdown. They are necessary as this is how
|
||||
|
||||
## Manually Loading Components
|
||||
|
||||
We can define as many components as we want within a page but typically it's best to define them in a separate files.
|
||||
We can define as many components as we want within a page but typically it's best to define them in separate files.
|
||||
|
||||
So we will move our component code into a new file 👉 `/site/src/components/rocket-greeting.js`
|
||||
|
||||
@@ -257,8 +257,8 @@ export const components = {
|
||||
|
||||
### Hydration
|
||||
|
||||
Component that do not have any interactivity will never need to be hydrated so they may be imported statically on the server side.
|
||||
All other component should be handled via the `components` object to enable handling of loading and registration.
|
||||
Components that do not have any interactivity will never need to be hydrated so they may be imported statically on the server side.
|
||||
All other components should be handled via the `components` object to enable handling of loading and registration.
|
||||
|
||||
Doing so enables hydration based on attributes on the component.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export const needsLoader = true;
|
||||
|
||||
# Layouts
|
||||
|
||||
Layout are special in that sense that they output the full html page (including html, head, body, etc).
|
||||
Layouts are special in the sense that they output the full html page (including html, head, body, etc).
|
||||
|
||||
The simplest layout you can make is
|
||||
|
||||
@@ -48,7 +48,7 @@ export const layout = data => html`
|
||||
`;
|
||||
```
|
||||
|
||||
and that will work fine however now every page will have the same `title` in the head.
|
||||
and that will work fine, however, now every page will have the same `title` in the head.
|
||||
|
||||
If we now have the following markdown file:
|
||||
|
||||
@@ -95,7 +95,7 @@ For more details see the following [lit issue](https://github.com/lit/lit/issues
|
||||
|
||||
</inline-notification>
|
||||
|
||||
In order to provide this `data.title` we now need to export is within the page.
|
||||
In order to provide this `data.title` we now need to export it within the page.
|
||||
The code could look something like this.
|
||||
|
||||
````md
|
||||
@@ -151,7 +151,7 @@ const layoutB =
|
||||
|
||||
<inline-notification type="warning">
|
||||
|
||||
Partial html is not supported in [lit](http://lit.dev) as it uses the browser build in html parser which try to "auto correct" your html by closing tags.
|
||||
Partial html is not supported in [lit](http://lit.dev) as it uses the browser build in html parser which trys to "auto correct" your html by closing tags.
|
||||
e.g. this
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
@@ -30,7 +30,7 @@ Rocket uses **file-based routing** to generate your build URLs based on the file
|
||||
|
||||
## Static routes
|
||||
|
||||
Rocket Pages are Markdown (`rocket.md`), HTML (`rocket.html`) and JavaScript (`rocket.js`) in the `site/pages` directory become pages on your website. Each page’s route is decided based on its filename and path within the `site/pages` directory. This means that there is no separate "routing config" to maintain in an Rocket project.
|
||||
Rocket Pages are Markdown (`rocket.md`), HTML (`rocket.html`) and JavaScript (`rocket.js`) in the `site/pages` directory become pages on your website. Each page’s route is decided based on its filename and path within the `site/pages` directory. This means that there is no separate "routing config" to maintain in a Rocket project.
|
||||
|
||||
```bash
|
||||
# Example: Static routes
|
||||
|
||||
@@ -76,7 +76,7 @@ This will result in something like this:
|
||||
</nav>
|
||||
```
|
||||
|
||||
Rocket comes with multiple build in menus you can see [below](#menu-types).
|
||||
Rocket comes with multiple built-in menus you can see [below](#menu-types).
|
||||
|
||||
## Menu Data
|
||||
|
||||
@@ -107,11 +107,11 @@ Now the menu will be called "Docs".
|
||||
Within a menu the text of the links is defined by the following priority:
|
||||
|
||||
1. menuLinkText => `export const menuLinkText = 'Page Title In Menu';`
|
||||
2. h1 => first `<h1>` in the page)
|
||||
2. h1 => first `<h1>` in the page
|
||||
3. title => html title tag
|
||||
4. sourceRelativeFilePath => fallback if no other option is available
|
||||
|
||||
You can influence that data that gets provided to the menu by setting exports.
|
||||
You can influence the data that gets provided to the menu by setting exports.
|
||||
|
||||
### link-text="..."
|
||||
|
||||
@@ -168,7 +168,7 @@ Sometimes there is a need to completely exclude a page from the pageTree.
|
||||
Pages with this flag will not exist at all in the pageTree - therefore you will not be able to access them for "anything" not even in a sitemap or an update feed.
|
||||
Pages that have sub pages can NOT use this flag as it would mean those sub pages would not have a parent page.
|
||||
|
||||
Typical use case are utility pages that are not meant to be accessed by typical users.
|
||||
Typical use cases are utility pages that are not meant to be accessed by typical users.
|
||||
|
||||
```js
|
||||
export const menuExclude = true;
|
||||
@@ -245,7 +245,7 @@ In case you want to take full control over the order you can apply the following
|
||||
- The menu order and file system order will no longer match
|
||||
- But no numbers in folder / filenames
|
||||
|
||||
3. Instantiate a new PageTree and providing your own `modelComparatorFn`
|
||||
3. Instantiate a new PageTree and provide your own `modelComparatorFn`
|
||||
|
||||
```js
|
||||
function modelComparatorFn(a, b) {
|
||||
@@ -261,7 +261,7 @@ In case you want to take full control over the order you can apply the following
|
||||
|
||||
## Page Tree
|
||||
|
||||
The data of the page tree gets saves as a JSON file in the root of the `pages` directory.
|
||||
The data of the page tree gets saved as a JSON file in the root of the `pages` directory.
|
||||
|
||||
It typically looks something like this:
|
||||
|
||||
@@ -440,7 +440,7 @@ import {
|
||||
5. **NextMenu**
|
||||
|
||||
- shows the next page in the tree
|
||||
- is either the first child or he next sibling
|
||||
- is either the first child or the next sibling
|
||||
|
||||
```html
|
||||
<web-menu name="next">
|
||||
|
||||
@@ -31,7 +31,7 @@ export const needsLoader = true;
|
||||
# Hydration
|
||||
|
||||
By default all components are only rendered on the server.
|
||||
This however means those components are "static" and can not add interactivity.
|
||||
However, this means those components are "static" and can not add interactivity.
|
||||
|
||||
To add interactivity to your component you can either render it only on the client or you can render on the server and `hydrate` it on the client.
|
||||
|
||||
@@ -69,9 +69,9 @@ The automatic loading/hydration only works if you [register components to the ro
|
||||
|
||||
## Server Loading
|
||||
|
||||
Server Loading has almost zero impact on the client side page performance and is therefore fastest possible solution available.
|
||||
Server Loading has almost zero impact on the client side page performance and is therefore the fastest possible solution available.
|
||||
|
||||
Loading and rendering on the server is the default and for that reasons does not need to be specified.
|
||||
Loading and rendering on the server is the default and for that reason does not need to be specified.
|
||||
|
||||
```html
|
||||
<blog-header></blog-header>
|
||||
@@ -263,7 +263,7 @@ Results in `server rendering` of all `my-component`s.
|
||||
Why theses impacts?
|
||||
|
||||
1. client - if you eagerly load & render the component on the client then bloating the html of some components by server rendering does not bring any benefit
|
||||
2. hydrate - hydration means that all component with the same tag name will be hydrated on the client - you can not keep a server only variation of a component
|
||||
2. hydrate - hydration means that all components with the same tag name will be hydrated on the client - you can not keep a server only variation of a component
|
||||
3. server - almost zero client side impact
|
||||
|
||||
<inline-notification>
|
||||
|
||||
@@ -103,7 +103,7 @@ If your package is `"type": "module"` then you can also add exports.
|
||||
}
|
||||
```
|
||||
|
||||
Once you have an export it also enables to "self" reference your package.
|
||||
Once you have an export it also enables you to "self" reference your package.
|
||||
Combine this with the `:resolve` function means that you can reference assets always with the same path.
|
||||
Completely unrelated to the path of the file itself.
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ For settings that may be overridden within a page, use the [**data cascade**](./
|
||||
|
||||
</inline-notification>
|
||||
|
||||
Rocket is primarily build around plugins for each of its systems.
|
||||
Rocket is primarily built around plugins for each of its systems.
|
||||
|
||||
Given that rocket comes with a list of default plugins and that presets can add plugins it's very important to not "just" set a new "plugins" property.
|
||||
|
||||
@@ -65,7 +65,7 @@ New plugins can be added and all default plugins can be adjusted or even removed
|
||||
```js
|
||||
/** @type {import('rocket/cli').RocketCliConfig} */
|
||||
export default {
|
||||
// add a rollup plugins to the web dev server (will be wrapped with @web/dev-server-rollup) AND the rollup build (e.g. enable json importing)
|
||||
// add a rollup plugin to the web dev server (will be wrapped with @web/dev-server-rollup) AND the rollup build (e.g. enable json importing)
|
||||
setupDevServerAndBuildPlugins: [],
|
||||
|
||||
// add a plugin to the web dev server (will not be wrapped) (e.g. esbuild for TypeScript)
|
||||
@@ -139,7 +139,7 @@ For some projects you might want to enable non-standard behaviors like importing
|
||||
import data from './data.json';
|
||||
```
|
||||
|
||||
You can accomplish this with Rollup and dev server plugins. Make sure to add both the dev-server plugin as well as the Rollup plugin, so that the behaviors is the same during development as it is in the production build.
|
||||
You can accomplish this with Rollup and dev server plugins. Make sure to add both the dev-server plugin as well as the Rollup plugin, so that the behavior is the same during development as it is in the production build.
|
||||
|
||||
For these cases you can use `setupDevServerAndBuildPlugins`, which will automatically add the plugin for you to both Rollup and dev-server:
|
||||
|
||||
@@ -186,7 +186,7 @@ export default /** @type {import('rocket/cli').RocketCliOptions} */ ({
|
||||
|
||||
### BuildOptions (rollup)
|
||||
|
||||
For example if you wanna add an `acron` plugin to rollup
|
||||
For example if you wanna add an `acorn` plugin to rollup
|
||||
|
||||
```js
|
||||
import { importAssertions } from 'acorn-import-assertions';
|
||||
|
||||
@@ -34,7 +34,7 @@ export const subTitle =
|
||||
Having a nice preview image for social media can be very helpful.
|
||||
For that reason Rocket has a specific functionality to generate a preview image.
|
||||
|
||||
This functionality is disable by default. You can enable it by exporting a `openGraphLayout` function/class instance.
|
||||
This functionality is disabled by default. You can enable it by exporting a `openGraphLayout` function/class instance.
|
||||
|
||||
The functionality is the same as normal [Layouts](../20--basics/50--layouts.rocket.md).
|
||||
|
||||
@@ -53,7 +53,7 @@ The functionality is the same as normal [Layouts](../20--basics/50--layouts.rock
|
||||
site/pages/sitemap.xml.rocket.js -> open graph only happens for html files
|
||||
```
|
||||
|
||||
2. During the the build phase is does the following steps
|
||||
2. During the build phase it does the following steps
|
||||
3. If there is a `filename.opengraph.html` file it will open that file in puppeteer
|
||||
4. It sets its screen size to 1200×628px and takes a screenshot with DPR or 2 (e.g. the image will have 2400x1228px)
|
||||
5. Adjusts the "output file" e.g. `index.html` by injecting
|
||||
@@ -66,13 +66,13 @@ The functionality is the same as normal [Layouts](../20--basics/50--layouts.rock
|
||||
|
||||
if no `<meta property="og:image"` is present
|
||||
|
||||
6. deletes the `filename.opengraph.html` file
|
||||
6. Deletes the `filename.opengraph.html` file
|
||||
|
||||
## How to use it
|
||||
|
||||
Add an `openGraphLayout` export to our page.
|
||||
|
||||
Generally it's probably bet if we put this in our root `recursive.data.js` file which means that every page will have this functionality.
|
||||
Generally it's probably best if we put this in our root `recursive.data.js` file which means that every page will have this functionality.
|
||||
If a certain page needs a different layout we can override it.
|
||||
|
||||
👉 `recursive.data.js`
|
||||
@@ -244,7 +244,7 @@ export const subTitle = 'From zero to hero';
|
||||
|
||||
## Creating an Overview
|
||||
|
||||
Once you have defined an openGraphLayout for all you pages it makes sense to check them.
|
||||
Once you have defined an openGraphLayout for all your pages it makes sense to check them.
|
||||
This however can be a time consuming process.
|
||||
|
||||
So it makes sense to use an overview over all the open graph images we will have.
|
||||
@@ -312,7 +312,7 @@ export const layout = data => html`
|
||||
`;
|
||||
```
|
||||
|
||||
There is quite some much in there so let's go though the code.
|
||||
There is quite a lot in there so let's go though the code.
|
||||
|
||||
```js
|
||||
import { pageTree } from '#src/layouts/layoutData.js';
|
||||
|
||||
@@ -39,7 +39,7 @@ Rocket does handle content images automatically by
|
||||
- producing multiple sizes so users download images that are meant for their resolution
|
||||
- outputting multiple formats so the device can choose the best image format it supports
|
||||
|
||||
And the best thing about is you don't need to do anything.
|
||||
And the best thing is you don't need to do anything.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -110,11 +110,11 @@ will result in
|
||||
|
||||
## Adjusting options
|
||||
|
||||
Under the hood it is using [11ty/image](https://www.11ty.dev/docs/plugins/image/) and all it's options are available.
|
||||
Under the hood it is using [11ty/image](https://www.11ty.dev/docs/plugins/image/) and all its options are available.
|
||||
|
||||
<inline-notification type="tip">
|
||||
|
||||
If you are using a layout preset like `@rocket/launch` then you probably don't want to touch/change these options as the preset will set it for you accordion to its layout needs.
|
||||
If you are using a layout preset like `@rocket/launch` then you probably don't want to touch/change these options as the preset will set it for you according to its layout needs.
|
||||
|
||||
The default preset for regular markdown content is available as `responsive`.
|
||||
|
||||
@@ -282,7 +282,7 @@ export default {
|
||||
|
||||
## Default widths
|
||||
|
||||
In order to understand the need for having a single image in multiple resolutions we need to understand the our website is served to many different environments and each may come with its own specific device pixel ratio (DPR). The device pixel ratio is the ratio between physical pixels and logical pixels. For instance, the Galaxy S20 report a device pixel ratio of 3, because the physical linear resolution is triple the logical linear resolution.
|
||||
In order to understand the need for having a single image in multiple resolutions we need to understand that our website is served to many different environments and each may come with its own specific device pixel ratio (DPR). The device pixel ratio is the ratio between physical pixels and logical pixels. For instance, the Galaxy S20 report a device pixel ratio of 3, because the physical linear resolution is triple the logical linear resolution.
|
||||
|
||||
Physical resolution: 1440 x 3200
|
||||
Logical resolution: 480 x 1067
|
||||
|
||||
@@ -68,6 +68,18 @@ rocket lint --build-html
|
||||
|
||||
Note: We can do this as 2-4 generally does not impact links/references (as long as the optimizations scripts do not have related bugs)
|
||||
|
||||
### Check external links
|
||||
|
||||
`rocket lint` can also check external links.
|
||||
This will do a HEAD/GET request to every external link which means if all is green that the website only links to still active websites.
|
||||
It however also means that it will do a lot of network requests which will take a while.
|
||||
|
||||
Use with care.
|
||||
|
||||
```bash
|
||||
rocket lint --validate-externals
|
||||
```
|
||||
|
||||
## Add a Not Found Page
|
||||
|
||||
When a user enters a URL that does not exist, a "famous" 404 Page Not Found error occurs.
|
||||
|
||||
@@ -35,7 +35,7 @@ export const subTitle = 'Use web fonts to ensure a unique and consistent experie
|
||||
Using web fonts can be tricky and as there are so many considerations
|
||||
|
||||
- Use a [Variable Font](https://web.dev/variable-fonts/)?
|
||||
- How to [reduced the size](https://web.dev/reduce-webfont-size/)?
|
||||
- How to [reduce the size](https://web.dev/reduce-webfont-size/)?
|
||||
- How to avoid [a layout shift as the font is loaded](https://web.dev/preload-optional-fonts/)?
|
||||
- ...
|
||||
|
||||
@@ -44,7 +44,7 @@ Here is a quick summary of what you should do as of 2022.
|
||||
1. Use a variable font
|
||||
|
||||
This means only ONE font file needs to be download for all the different weights and widths.
|
||||
This file is usually bigger then one weight of a font but smaller then multiple weights font files.
|
||||
This file is usually bigger than one weight of a font but smaller than multiple weights font files.
|
||||
|
||||
Not many fonts are "easily" accessible as a variable font. Often you need to manually [convert](https://convertio.co/ttf-woff/) a variable font ttf file to a web woff2 file.
|
||||
|
||||
@@ -75,7 +75,7 @@ Here is a quick summary of what you should do as of 2022.
|
||||
|
||||
3. Use optional fonts
|
||||
|
||||
In combination with 2. this means there will be NO layout shift at all. Nothing will be display until the font is loaded or a timeout (usually 100ms) is reached. If there is a timeout then for this page visit a fallback font will be used.
|
||||
In combination with 2. this means there will be NO layout shift at all. Nothing will be displayed until the font is loaded or a timeout (usually 100ms) is reached. If there is a timeout then for this page visit a fallback font will be used.
|
||||
On the next page load the font will be cached and directly rendered with the web font.
|
||||
|
||||
```css
|
||||
|
||||
@@ -53,7 +53,7 @@ export const layout = new LayoutSidebar();
|
||||
```
|
||||
|
||||
You should also define it as a preset in the configuration so that it can copy some default public files.
|
||||
(this step is not requires but it is recommended)
|
||||
(this step is not required but it is recommended)
|
||||
|
||||
👉 `config/rocket.config.js`
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ The registration happens via
|
||||
<script type="module" src="resolve:@rocket/launch/js/register-service-worker.js"></script>
|
||||
```
|
||||
|
||||
Below you find its implementation
|
||||
Below you'll find its implementation
|
||||
|
||||
```js server
|
||||
const serviceWorkerRegistrationCode = await inlineFile(
|
||||
|
||||
@@ -60,6 +60,8 @@ npm i -D check-html-links
|
||||
| root-dir | string | the root directory to serve files from. Defaults to the current working directory |
|
||||
| ignore-link-pattern | string | do not check links matching the pattern |
|
||||
| continue-on-error | boolean | if present it will not exit with an error code - useful while writing or for temporary passing a ci |
|
||||
| validate-externals | boolean | if present it will validate external links making a request to the linked url |
|
||||
| absolute-base-url | string | the urls of your website - if provided it will handle absolute links that start with it as internal |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
@@ -72,6 +74,12 @@ npx check-html-links _site --ignore-link-pattern "/users/*" "/users/**/*"
|
||||
|
||||
# ignore all links like <a href="/users/123"> & <a href="/users/123/details">
|
||||
npx check-html-links _site --ignore-link-pattern "/users/*" "/users/**/*"
|
||||
|
||||
# check external urls
|
||||
npx check-html-links _site --validate-externals
|
||||
|
||||
# check external urls but treat links like https://rocket.modern-web.dev/about/ as internal
|
||||
npx check-html-links _site --validate-externals --absolute-base-url https://rocket.modern-web.dev
|
||||
```
|
||||
|
||||
## Example Output
|
||||
|
||||
@@ -1373,6 +1373,11 @@
|
||||
"id": "fixing-broken-links",
|
||||
"level": 3
|
||||
},
|
||||
{
|
||||
"text": "Check external links",
|
||||
"id": "check-external-links",
|
||||
"level": 3
|
||||
},
|
||||
{
|
||||
"text": "Add a Not Found Page",
|
||||
"id": "add-a-not-found-page",
|
||||
|
||||
@@ -6109,14 +6109,14 @@ node-emoji@^1.10.0:
|
||||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
|
||||
node-fetch@2.6.7, node-fetch@^2.6.7:
|
||||
node-fetch@2.6.7:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-fetch@^3.2.8:
|
||||
node-fetch@^3.0.0, node-fetch@^3.2.8:
|
||||
version "3.2.10"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8"
|
||||
integrity sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==
|
||||
|
||||
Reference in New Issue
Block a user