Port website to Astro & Tailwind (#5009)
4
.github/workflows/lint.yml
vendored
@@ -36,5 +36,9 @@ jobs:
|
||||
- name: Build Cheerio
|
||||
run: npm run build
|
||||
|
||||
- name: Build website
|
||||
run: npm run build
|
||||
working-directory: website
|
||||
|
||||
- name: Run lint
|
||||
run: npm run lint
|
||||
|
||||
7
.gitignore
vendored
@@ -7,5 +7,8 @@ npm-debug.log
|
||||
/.tshy
|
||||
/.tshy-build
|
||||
/dist
|
||||
/website/docs/api
|
||||
/website/build
|
||||
|
||||
# Website build artifacts
|
||||
website/.astro/
|
||||
website/dist/
|
||||
website/src/content/docs/api
|
||||
|
||||
@@ -15,14 +15,14 @@ const gitignorePath = fileURLToPath(new URL('.gitignore', import.meta.url));
|
||||
export default defineConfig(
|
||||
includeIgnoreFile(gitignorePath), // Handle .gitignore patterns
|
||||
|
||||
// 0. Global linter options
|
||||
// Global linter options
|
||||
{
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: true, // Enable reporting of unused disable directives
|
||||
},
|
||||
},
|
||||
|
||||
// 1. Base configurations for all relevant files
|
||||
// Base configurations for all relevant files
|
||||
eslintJs.configs.recommended, // Basic ESLint recommended rules
|
||||
|
||||
{
|
||||
@@ -77,13 +77,14 @@ export default defineConfig(
|
||||
},
|
||||
},
|
||||
|
||||
// 2. Global custom rules and language options
|
||||
// Global custom rules and language options
|
||||
{
|
||||
languageOptions: {
|
||||
globals: globals.node,
|
||||
parserOptions: {
|
||||
projectService: {
|
||||
allowDefaultProject: ['*.js'],
|
||||
defaultProject: 'tsconfig.json',
|
||||
},
|
||||
tsconfigRootDir: import.meta.dirname, // eslint-disable-line n/no-unsupported-features/node-builtins
|
||||
},
|
||||
@@ -117,7 +118,7 @@ export default defineConfig(
|
||||
},
|
||||
},
|
||||
|
||||
// 3. TypeScript specific configurations
|
||||
// TypeScript specific configurations
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
{
|
||||
@@ -164,7 +165,7 @@ export default defineConfig(
|
||||
},
|
||||
},
|
||||
|
||||
// 4. Vitest specific configuration (for *.spec.ts files)
|
||||
// Vitest specific configuration (for *.spec.ts files)
|
||||
{
|
||||
files: ['**/*.spec.ts'],
|
||||
plugins: { vitest: eslintPluginVitest },
|
||||
@@ -182,6 +183,19 @@ export default defineConfig(
|
||||
},
|
||||
},
|
||||
|
||||
// 5. Prettier - must be the last configuration to override styling rules
|
||||
// Website specific configuration
|
||||
{
|
||||
files: ['website/**/*.ts', 'website/**/*.tsx', 'website/**/*.mts'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: {
|
||||
allowDefaultProject: ['*.mjs'],
|
||||
},
|
||||
tsconfigRootDir: `${import.meta.dirname}/website`, // eslint-disable-line n/no-unsupported-features/node-builtins
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Prettier - must be the last configuration to override styling rules
|
||||
eslintConfigPrettier,
|
||||
);
|
||||
|
||||
35
website/astro.config.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import mdx from '@astrojs/mdx';
|
||||
import react from '@astrojs/react';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import remarkDirective from 'remark-directive';
|
||||
import { remarkAdmonitions } from './src/plugins/remark-admonitions.ts';
|
||||
import { remarkFixTypedocLinks } from './src/plugins/remark-fix-typedoc-links.ts';
|
||||
import { remarkLiveCode } from './src/plugins/remark-live-code.ts';
|
||||
import { rehypeExternalLinks } from './src/plugins/rehype-external-links.ts';
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://cheerio.js.org',
|
||||
integrations: [mdx(), react(), sitemap()],
|
||||
image: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'unavatar.io',
|
||||
},
|
||||
],
|
||||
},
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
markdown: {
|
||||
remarkPlugins: [
|
||||
remarkDirective,
|
||||
remarkAdmonitions,
|
||||
remarkFixTypedocLinks,
|
||||
remarkLiveCode,
|
||||
],
|
||||
rehypePlugins: [rehypeExternalLinks],
|
||||
},
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
fb55:
|
||||
name: Felix Boehm
|
||||
title: Maintainer of Cheerio
|
||||
url: https://feedic.com/
|
||||
image_url: https://github.com/fb55.png
|
||||
socials:
|
||||
github: fb55
|
||||
@@ -1,13 +0,0 @@
|
||||
project_id: '567683'
|
||||
api_token_env: CROWDIN_PERSONAL_TOKEN
|
||||
preserve_hierarchy: true
|
||||
files:
|
||||
# JSON translation files
|
||||
- source: /i18n/en/**/*
|
||||
translation: /i18n/%two_letters_code%/**/%original_file_name%
|
||||
# Docs Markdown files
|
||||
- source: /docs/**/*
|
||||
translation: /i18n/%two_letters_code%/docusaurus-plugin-content-docs/current/**/%original_file_name%
|
||||
# Blog Markdown files
|
||||
- source: /blog/**/*
|
||||
translation: /i18n/%two_letters_code%/docusaurus-plugin-content-blog/**/%original_file_name%
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"label": "Tutorials - Advanced",
|
||||
"position": 3,
|
||||
"link": {
|
||||
"type": "generated-index"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"label": "Tutorials - Basics",
|
||||
"position": 2,
|
||||
"link": {
|
||||
"type": "generated-index"
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { themes } = require('prism-react-renderer');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: packageJson.name,
|
||||
tagline: packageJson.description,
|
||||
url: packageJson.homepage,
|
||||
baseUrl: '/',
|
||||
trailingSlash: false,
|
||||
onBrokenLinks: 'warn',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
favicon: 'img/favicon.ico',
|
||||
|
||||
// GitHub pages deployment config.
|
||||
organizationName: 'cheeriojs', // Usually your GitHub org/user name.
|
||||
projectName: 'cheerio', // Usually your repo name.
|
||||
|
||||
/*
|
||||
* Even if you don't use internalization, you can use this field to set useful
|
||||
* metadata like html lang. For example, if your site is Chinese, you may want
|
||||
* to replace "en" with "zh-Hans".
|
||||
*/
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en'],
|
||||
},
|
||||
|
||||
future: {
|
||||
experimental_faster: true,
|
||||
v4: true,
|
||||
},
|
||||
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||
({
|
||||
docs: {
|
||||
editUrl: 'https://github.com/cheeriojs/cheerio/tree/main/website/',
|
||||
remarkPlugins: [
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
[require('@docusaurus/remark-plugin-npm2yarn'), { sync: true }],
|
||||
],
|
||||
},
|
||||
blog: {
|
||||
showReadingTime: true,
|
||||
editUrl: 'https://github.com/cheeriojs/cheerio/tree/main/website/',
|
||||
},
|
||||
theme: {
|
||||
customCss: require.resolve('./src/css/custom.css'),
|
||||
},
|
||||
gtag: {
|
||||
trackingID: 'G-PZHRH775FB',
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||
({
|
||||
navbar: {
|
||||
title: 'Cheerio',
|
||||
logo: {
|
||||
alt: 'Cheerio Logo',
|
||||
src: 'img/orange-c.svg',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
docId: 'intro',
|
||||
position: 'left',
|
||||
label: 'Tutorial',
|
||||
},
|
||||
{
|
||||
to: 'docs/api',
|
||||
label: 'API',
|
||||
position: 'left',
|
||||
},
|
||||
{ to: '/blog', label: 'Blog', position: 'left' },
|
||||
{
|
||||
href: 'https://github.com/cheeriojs/cheerio',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: 'dark',
|
||||
links: [
|
||||
{
|
||||
title: 'Docs',
|
||||
items: [
|
||||
{
|
||||
label: 'Tutorial',
|
||||
to: '/docs/intro',
|
||||
},
|
||||
{
|
||||
label: 'API',
|
||||
to: 'docs/api',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
items: [
|
||||
{
|
||||
label: 'Stack Overflow',
|
||||
href: 'https://stackoverflow.com/questions/tagged/cheerio',
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/cheeriojs/cheerio',
|
||||
},
|
||||
/*
|
||||
* {
|
||||
* label: 'Twitter',
|
||||
* href: 'https://twitter.com/docusaurus',
|
||||
* },
|
||||
*/
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'More',
|
||||
items: [
|
||||
{
|
||||
label: 'Blog',
|
||||
to: '/blog',
|
||||
},
|
||||
{
|
||||
label: 'Attribution',
|
||||
href: '/attribution',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} The Cheerio contributors`,
|
||||
},
|
||||
metadata: [
|
||||
{
|
||||
name: 'keywords',
|
||||
content: `${packageJson.keywords.join(', ')}, nodejs`,
|
||||
},
|
||||
],
|
||||
prism: {
|
||||
theme: themes.github,
|
||||
darkTheme: themes.dracula,
|
||||
},
|
||||
algolia: {
|
||||
appId: 'NRR2XU4QSP',
|
||||
apiKey: '9d30ee79d65ccc63b95e693124e05405',
|
||||
indexName: 'crawler_cheerio',
|
||||
},
|
||||
}),
|
||||
|
||||
themes: ['@docusaurus/theme-live-codeblock'],
|
||||
|
||||
plugins: [
|
||||
[
|
||||
'client-redirects',
|
||||
/** @type {import('@docusaurus/plugin-client-redirects').Options} */
|
||||
({
|
||||
fromExtensions: ['html'],
|
||||
redirects: [
|
||||
// Classes
|
||||
{
|
||||
from: `/classes/Cheerio.html`,
|
||||
to: `/docs/api/classes/Cheerio`,
|
||||
},
|
||||
|
||||
// Interfaces
|
||||
...['CheerioAPI', 'CheerioOptions', 'HTMLParser2Options'].map(
|
||||
(name) => ({
|
||||
from: `/interfaces/${name}.html`,
|
||||
to: `/docs/api/interfaces/${name}`,
|
||||
}),
|
||||
),
|
||||
|
||||
// Type aliases and functions
|
||||
|
||||
{
|
||||
/*
|
||||
* Type aliases and functions are all part of the `api` page. We
|
||||
* unfortunately can't redirect to a specific function, so we
|
||||
* redirect to the top of the page.
|
||||
*/
|
||||
from: [
|
||||
...[
|
||||
'AcceptedElems',
|
||||
'AcceptedFilters',
|
||||
'AnyNode',
|
||||
'BasicAcceptedElems',
|
||||
'FilterFunction',
|
||||
'ParentNode',
|
||||
'SelectorType',
|
||||
].map((name) => `/types/${name}.html`),
|
||||
|
||||
...[
|
||||
'contains',
|
||||
'default',
|
||||
'html',
|
||||
'load',
|
||||
'merge',
|
||||
'parseHTML',
|
||||
'root',
|
||||
'text',
|
||||
'xml',
|
||||
].map((name) => `/functions/${name}.html`),
|
||||
],
|
||||
to: '/docs/api',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
[
|
||||
'docusaurus-plugin-typedoc',
|
||||
{
|
||||
// TypeDoc options
|
||||
entryPoints: ['../src/index.ts'],
|
||||
tsconfig: '../tsconfig.typedoc.json',
|
||||
readme: 'none',
|
||||
excludePrivate: true,
|
||||
|
||||
externalSymbolLinkMappings: {
|
||||
domhandler: {
|
||||
Document: 'https://domhandler.js.org/classes/Document.html',
|
||||
Element: 'https://domhandler.js.org/classes/Element.html',
|
||||
Node: 'https://domhandler.js.org/classes/Node.html',
|
||||
|
||||
AnyNode: 'https://domhandler.js.org/types/AnyNode.html',
|
||||
ChildNode: 'https://domhandler.js.org/types/ChildNode.html',
|
||||
ParentNode: 'https://domhandler.js.org/types/ParentNode.html',
|
||||
|
||||
DomHandlerOptions:
|
||||
'https://domhandler.js.org/interfaces/DomHandlerOptions.html',
|
||||
},
|
||||
parse5: {
|
||||
ParserOptions:
|
||||
'https://parse5.js.org/interfaces/parse5.ParserOptions.html',
|
||||
},
|
||||
},
|
||||
|
||||
plugin: ['typedoc-plugin-mdn-links'],
|
||||
|
||||
// Plugin options
|
||||
sidebar: {
|
||||
// Always display the API entry last
|
||||
position: Number.MAX_SAFE_INTEGER,
|
||||
pretty: true,
|
||||
},
|
||||
outputFileStrategy: 'members',
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
19113
website/package-lock.json
generated
@@ -1,56 +1,43 @@
|
||||
{
|
||||
"name": "@cheerio/website",
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"crowdin:sync": "docusaurus write-translations && crowdin upload && crowdin download",
|
||||
"lint:ts": "tsc --noEmit"
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "npm run build:api && astro build",
|
||||
"build:api": "typedoc",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^3.9.2",
|
||||
"@docusaurus/module-type-aliases": "^3.9.2",
|
||||
"@docusaurus/plugin-client-redirects": "^3.9.2",
|
||||
"@docusaurus/preset-classic": "^3.9.2",
|
||||
"@docusaurus/remark-plugin-npm2yarn": "^3.9.2",
|
||||
"@docusaurus/theme-live-codeblock": "^3.9.2",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"clsx": "^2.1.1",
|
||||
"docusaurus-plugin-typedoc": "^1.4.2",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^19.2.3",
|
||||
"@astrojs/mdx": "^4.3.0",
|
||||
"@astrojs/react": "^4.3.0",
|
||||
"@astrojs/rss": "^4.0.15",
|
||||
"@astrojs/sitemap": "^3.7.0",
|
||||
"@codesandbox/sandpack-react": "^2.19.0",
|
||||
"@docsearch/css": "^4.4.0",
|
||||
"@docsearch/js": "^4.4.0",
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"astro": "^5.9.0",
|
||||
"hastscript": "^9.0.1",
|
||||
"marked": "^17.0.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.2.3",
|
||||
"typedoc": "^0.28.16",
|
||||
"typedoc-plugin-markdown": "^4.9.0",
|
||||
"typedoc-plugin-mdn-links": "^5.1.0"
|
||||
"remark-directive": "^4.0.0",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@crowdin/cli": "^4.12.0",
|
||||
"@docusaurus/faster": "^3.9.2",
|
||||
"@tsconfig/docusaurus": "^2.0.7",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.0",
|
||||
"typedoc": "^0.28.5",
|
||||
"typedoc-plugin-markdown": "^4.6.3",
|
||||
"typedoc-plugin-mdn-links": "^5.0.1",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14"
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 724 B After Width: | Height: | Size: 724 B |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 955 B After Width: | Height: | Size: 955 B |
47
website/src/components/Features.astro
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
const features = [
|
||||
{
|
||||
title: 'Proven syntax',
|
||||
icon: '/img/1F496.svg',
|
||||
description:
|
||||
"Cheerio implements a subset of core jQuery. Cheerio removes all the DOM inconsistencies and browser cruft from the jQuery library, revealing its truly gorgeous API.",
|
||||
},
|
||||
{
|
||||
title: 'Blazingly fast',
|
||||
icon: '/img/26A1.svg',
|
||||
description:
|
||||
'Cheerio works with a very simple, consistent DOM model. As a result parsing, manipulating, and rendering are incredibly efficient.',
|
||||
},
|
||||
{
|
||||
title: 'Incredibly flexible',
|
||||
icon: '/img/1F57A.svg',
|
||||
description:
|
||||
'Cheerio can parse nearly any HTML or XML document. Cheerio works in both browser and server environments.',
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<section class="bg-white py-16 dark:bg-slate-900">
|
||||
<div class="mx-auto max-w-7xl px-4">
|
||||
<div class="grid grid-cols-1 gap-12 md:grid-cols-3">
|
||||
{
|
||||
features.map((feature) => (
|
||||
<div class="text-center">
|
||||
<div class="mb-4 flex justify-center">
|
||||
<img
|
||||
src={feature.icon}
|
||||
alt=""
|
||||
class="h-32 w-32"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="mb-2 text-xl font-semibold">{feature.title}</h3>
|
||||
<p class="text-slate-600 dark:text-slate-400">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
56
website/src/components/Footer.astro
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
const footerLinks = {
|
||||
Docs: [
|
||||
{ label: 'Tutorial', href: '/docs/intro' },
|
||||
{ label: 'API', href: '/docs/api' },
|
||||
],
|
||||
Community: [
|
||||
{
|
||||
label: 'Stack Overflow',
|
||||
href: 'https://stackoverflow.com/questions/tagged/cheerio',
|
||||
},
|
||||
{ label: 'GitHub', href: 'https://github.com/cheeriojs/cheerio' },
|
||||
],
|
||||
More: [
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
{ label: 'Attribution', href: '/attribution' },
|
||||
],
|
||||
};
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<footer class="bg-slate-900 py-12 text-slate-300">
|
||||
<div class="mx-auto max-w-7xl px-4">
|
||||
<div class="grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||
{
|
||||
Object.entries(footerLinks).map(([title, links]) => (
|
||||
<div>
|
||||
<h3 class="mb-4 font-semibold text-white">{title}</h3>
|
||||
<ul class="space-y-2">
|
||||
{links.map((link) => (
|
||||
<li>
|
||||
<a
|
||||
href={link.href}
|
||||
class="transition-colors hover:text-primary-light"
|
||||
target={link.href.startsWith('http') ? '_blank' : undefined}
|
||||
rel={
|
||||
link.href.startsWith('http')
|
||||
? 'noopener noreferrer'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div class="mt-8 border-t border-slate-700 pt-8 text-center text-sm">
|
||||
Copyright © {currentYear} The Cheerio contributors
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
18
website/src/components/Hero.astro
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
const title = 'cheerio';
|
||||
const tagline =
|
||||
'The fast, flexible & elegant library for parsing and manipulating HTML and XML.';
|
||||
---
|
||||
|
||||
<header class="bg-primary py-16 text-center text-white">
|
||||
<div class="mx-auto max-w-4xl px-4">
|
||||
<h1 class="mb-4 text-5xl font-bold">{title}</h1>
|
||||
<p class="mb-8 text-xl opacity-90">{tagline}</p>
|
||||
<a
|
||||
href="/docs/intro"
|
||||
class="inline-block rounded-lg bg-white px-8 py-3 font-semibold text-primary transition-colors hover:bg-slate-100"
|
||||
>
|
||||
Get Started!
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
11
website/src/components/LiveEditor.astro
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
/**
|
||||
* Astro component that wraps LiveCode for use in markdown.
|
||||
* Usage in markdown: <LiveEditor>...</LiveEditor>
|
||||
*/
|
||||
import { LiveCode } from './live-code';
|
||||
---
|
||||
|
||||
<div class="live-editor-wrapper">
|
||||
<LiveCode client:load code={await Astro.slots.render('default')} />
|
||||
</div>
|
||||
99
website/src/components/Navbar.astro
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
const navItems = [
|
||||
{ label: 'Tutorial', href: '/docs/intro' },
|
||||
{ label: 'API', href: '/docs/api' },
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
];
|
||||
---
|
||||
|
||||
<nav class="sticky top-0 z-50 border-b border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900">
|
||||
<div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-3">
|
||||
<div class="flex items-center gap-8">
|
||||
<a href="/" class="flex items-center gap-2">
|
||||
<img src="/img/orange-c.svg" alt="Cheerio Logo" class="h-8 w-8" />
|
||||
<span class="text-xl font-semibold">Cheerio</span>
|
||||
</a>
|
||||
|
||||
<div class="hidden items-center gap-6 md:flex">
|
||||
{
|
||||
navItems.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
class="text-sm font-medium text-slate-600 transition-colors hover:text-primary dark:text-slate-300 dark:hover:text-primary-light"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- DocSearch container -->
|
||||
<div id="docsearch" class="hidden md:block"></div>
|
||||
|
||||
<a
|
||||
href="https://github.com/cheeriojs/cheerio"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-slate-600 transition-colors hover:text-primary dark:text-slate-300"
|
||||
aria-label="GitHub"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" class="h-6 w-6" fill="currentColor">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-md p-2 text-slate-600 hover:bg-slate-100 hover:text-slate-900 md:hidden dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
aria-label="Open menu"
|
||||
id="mobile-menu-button"
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu -->
|
||||
<div id="mobile-menu" class="hidden md:hidden">
|
||||
<div class="space-y-1 px-4 pb-3 pt-2">
|
||||
{
|
||||
navItems.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
class="block rounded-md px-3 py-2 text-base font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
// Mobile menu toggle
|
||||
const menuButton = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
|
||||
menuButton?.addEventListener('click', () => {
|
||||
mobileMenu?.classList.toggle('hidden');
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Initialize DocSearch
|
||||
import docsearch from '@docsearch/js';
|
||||
import '@docsearch/css';
|
||||
|
||||
docsearch({
|
||||
appId: 'NRR2XU4QSP',
|
||||
apiKey: '9d30ee79d65ccc63b95e693124e05405',
|
||||
indexName: 'crawler_cheerio',
|
||||
container: '#docsearch',
|
||||
});
|
||||
</script>
|
||||
78
website/src/components/Sidebar.astro
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
interface SidebarItem {
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface SidebarSection {
|
||||
title: string;
|
||||
items: SidebarItem[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
currentPath: string;
|
||||
}
|
||||
|
||||
const { currentPath } = Astro.props;
|
||||
|
||||
const sidebar: SidebarSection[] = [
|
||||
{
|
||||
title: 'Getting Started',
|
||||
items: [{ label: 'Introduction', href: '/docs/intro' }],
|
||||
},
|
||||
{
|
||||
title: 'Basics',
|
||||
items: [
|
||||
{ label: 'Loading Documents', href: '/docs/basics/loading' },
|
||||
{ label: 'Selecting Elements', href: '/docs/basics/selecting' },
|
||||
{ label: 'Traversing the DOM', href: '/docs/basics/traversing' },
|
||||
{ label: 'Manipulating Elements', href: '/docs/basics/manipulation' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Advanced',
|
||||
items: [
|
||||
{ label: 'Configuring Cheerio', href: '/docs/advanced/configuring-cheerio' },
|
||||
{ label: 'Extending Cheerio', href: '/docs/advanced/extending-cheerio' },
|
||||
{ label: 'Extracting Data', href: '/docs/advanced/extract' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Reference',
|
||||
items: [{ label: 'API Documentation', href: '/docs/api' }],
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<aside
|
||||
class="hidden w-64 shrink-0 border-r border-slate-200 dark:border-slate-700 lg:block"
|
||||
>
|
||||
<nav class="sticky top-0 h-screen overflow-y-auto px-6 py-8">
|
||||
{
|
||||
sidebar.map((section, index) => (
|
||||
<div class={index === 0 ? 'mb-8' : 'mb-8'}>
|
||||
<h3 class="mb-3 text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500">
|
||||
{section.title}
|
||||
</h3>
|
||||
<ul class="space-y-1">
|
||||
{section.items.map((item) => (
|
||||
<li>
|
||||
<a
|
||||
href={item.href}
|
||||
class:list={[
|
||||
'block rounded-lg px-3 py-2 text-sm transition-all duration-150',
|
||||
currentPath === item.href
|
||||
? 'bg-primary/10 font-semibold text-primary border-l-2 border-primary'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-slate-100',
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
</aside>
|
||||
47
website/src/components/Sponsors.astro
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
import sponsorsData from '../../sponsors.json';
|
||||
|
||||
interface Sponsor {
|
||||
name: string;
|
||||
image: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const headliners = sponsorsData.headliner as Sponsor[];
|
||||
---
|
||||
|
||||
<section class="bg-slate-50 py-12 dark:bg-slate-800">
|
||||
<div class="mx-auto max-w-7xl px-4">
|
||||
<h2 class="mb-8 text-center text-2xl font-semibold">
|
||||
Supported and Backed by
|
||||
</h2>
|
||||
<div class="flex flex-wrap items-center justify-center gap-8">
|
||||
{
|
||||
headliners.map((sponsor) => (
|
||||
<a
|
||||
href={sponsor.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center gap-3 rounded-lg p-3 transition-colors hover:bg-slate-100 dark:hover:bg-slate-700"
|
||||
>
|
||||
<img
|
||||
src={sponsor.image}
|
||||
alt={`${sponsor.name} logo`}
|
||||
class="h-12 w-12 rounded-lg"
|
||||
/>
|
||||
<span class="font-medium">{sponsor.name}</span>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
<a
|
||||
href="https://github.com/sponsors/cheeriojs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center gap-3 rounded-lg border border-slate-300 p-3 transition-all hover:border-primary hover:shadow-md dark:border-slate-600 dark:hover:border-primary-light"
|
||||
>
|
||||
<img src="/img/1F496.svg" alt="" class="h-12 w-12" aria-hidden="true" />
|
||||
<span class="font-medium">...and you?</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
39
website/src/components/TableOfContents.astro
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
interface Props {
|
||||
headings: Array<{
|
||||
depth: number;
|
||||
slug: string;
|
||||
text: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const { headings } = Astro.props;
|
||||
|
||||
// Filter to only h2 and h3 headings
|
||||
const toc = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
|
||||
---
|
||||
|
||||
{toc.length > 0 && (
|
||||
<nav class="hidden xl:block w-56 shrink-0 pl-6 pt-4">
|
||||
<div class="sticky top-8">
|
||||
<h4 class="mb-3 text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500">
|
||||
On this page
|
||||
</h4>
|
||||
<ul class="space-y-2 text-xs">
|
||||
{toc.map((heading) => (
|
||||
<li>
|
||||
<a
|
||||
href={`#${heading.slug}`}
|
||||
class:list={[
|
||||
'block text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200 transition-colors leading-snug',
|
||||
heading.depth === 2 ? 'font-medium text-slate-600 dark:text-slate-300' : 'pl-3 text-slate-500',
|
||||
]}
|
||||
>
|
||||
{heading.text}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
132
website/src/components/Testimonials.astro
Normal file
@@ -0,0 +1,132 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
|
||||
interface Tweet {
|
||||
id: string;
|
||||
name: string;
|
||||
user: string;
|
||||
date: string;
|
||||
tweet: string;
|
||||
}
|
||||
|
||||
const tweets: Tweet[] = [
|
||||
{
|
||||
id: '628016191928446977',
|
||||
name: 'Axel Rauschmayer',
|
||||
user: 'rauschma',
|
||||
date: '2015-08-03T02:35:00.000Z',
|
||||
tweet:
|
||||
"For transforming HTML via Node.js scripts, @mattmueller's cheerio works really well.",
|
||||
},
|
||||
{
|
||||
id: '1616150822932385792',
|
||||
name: 'Valeri Karpov',
|
||||
user: 'code_barbarian',
|
||||
date: '2023-01-19T19:09:00.000Z',
|
||||
tweet:
|
||||
"Cheerio is a weird npm module: most devs have never heard of it, but I rarely build an app without it. So much utility for quick and easy HTML transformations.",
|
||||
},
|
||||
{
|
||||
id: '1545481085865320449',
|
||||
name: 'Thomas Boutell',
|
||||
user: 'boutell',
|
||||
date: '2021-07-08T19:52:00.000Z',
|
||||
tweet:
|
||||
"You probably shouldn't use jQuery, but if you're great at jQuery, you're going to be really popular on server-side projects that need web scraping or HTML transformation. \"npm install cheerio\" ahoy!",
|
||||
},
|
||||
{
|
||||
id: '552311181760008192',
|
||||
name: 'Alistair G MacDonald',
|
||||
user: 'html5js',
|
||||
date: '2015-01-06T03:50:00.000Z',
|
||||
tweet:
|
||||
'Looking for a faster, cleaner alternative to basic JSDOM? Try Cheerio! #npm #javascript #nodejs',
|
||||
},
|
||||
{
|
||||
id: '1466753169900273667',
|
||||
name: 'Yogini Bende',
|
||||
user: 'hey_yogini',
|
||||
date: '2021-12-03T12:56:00.000Z',
|
||||
tweet: 'Cheerio is fire',
|
||||
},
|
||||
{
|
||||
id: '936243649234591744',
|
||||
name: 'Jonny Frodsham',
|
||||
user: 'jonnyfrodsham',
|
||||
date: '2017-11-30T14:40:00.000Z',
|
||||
tweet:
|
||||
"Needed to do a quick web scrape in Node for a demo. Seems like I'm back using jQuery in the super timesaving cheerio npm package",
|
||||
},
|
||||
{
|
||||
id: '264033999272439809',
|
||||
name: 'Thomas Steiner',
|
||||
user: 'tomayac',
|
||||
date: '2012-11-01T15:59:00.000Z',
|
||||
tweet:
|
||||
"npm install cheerio. That's the #jQuery DOM API for #nodeJS essentially. Thanks, @MattMueller",
|
||||
},
|
||||
{
|
||||
id: '1403139379757977602',
|
||||
name: 'Mike Pennisi',
|
||||
user: 'JugglinMike',
|
||||
date: '2021-06-11T04:57:00.000Z',
|
||||
tweet:
|
||||
"Thank you @fb55 for tirelessly pushing Cheerio to version 1.0. That library helps so many developers expand their horizons beyond the browser, and you've been making it possible for a decade!",
|
||||
},
|
||||
{
|
||||
id: '1186972238190403587',
|
||||
name: 'Matthew Phillips',
|
||||
user: 'matthewcp',
|
||||
date: '2019-10-23T12:46:00.000Z',
|
||||
tweet:
|
||||
'Cheerio is (still) such a useful tool for manipulating HTML. Shout to @MattMueller for saving me an untold amount of time over the years.',
|
||||
},
|
||||
];
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
---
|
||||
|
||||
<section class="py-16">
|
||||
<div class="mx-auto max-w-7xl px-4">
|
||||
<h2 class="mb-8 text-center text-2xl font-semibold">What Our Users Say</h2>
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{
|
||||
tweets.map((tweet) => (
|
||||
<div class="rounded-lg border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-700 dark:bg-slate-800">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<Image
|
||||
src={`https://unavatar.io/${tweet.user}`}
|
||||
alt={`${tweet.name}'s avatar`}
|
||||
width={40}
|
||||
height={40}
|
||||
class="h-10 w-10 rounded-full"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div>
|
||||
<div class="font-medium">{tweet.name}</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">
|
||||
@{tweet.user}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-4 text-slate-700 dark:text-slate-300">{tweet.tweet}</p>
|
||||
<a
|
||||
href={`https://twitter.com/${tweet.user}/status/${tweet.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm text-slate-500 transition-colors hover:text-primary dark:text-slate-400"
|
||||
>
|
||||
{formatDate(tweet.date)}
|
||||
</a>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,70 +0,0 @@
|
||||
import React from 'react';
|
||||
import Heart from '@site/static/img/1F496.svg';
|
||||
import Lightning from '@site/static/img/26A1.svg';
|
||||
import PersonRunning from '@site/static/img/1F57A.svg';
|
||||
|
||||
interface FeatureItem {
|
||||
title: string;
|
||||
Svg: React.ComponentType<React.ComponentProps<'svg'>>;
|
||||
description: JSX.Element;
|
||||
}
|
||||
|
||||
const FeatureList: FeatureItem[] = [
|
||||
{
|
||||
title: 'Proven syntax',
|
||||
Svg: Heart,
|
||||
description: (
|
||||
<>
|
||||
Cheerio implements a subset of core jQuery. Cheerio removes all the DOM
|
||||
inconsistencies and browser cruft from the jQuery library, revealing its
|
||||
truly gorgeous API.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Blazingly fast',
|
||||
Svg: Lightning,
|
||||
description: (
|
||||
<>
|
||||
Cheerio works with a very simple, consistent DOM model. As a result
|
||||
parsing, manipulating, and rendering are incredibly efficient.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Incredibly flexible',
|
||||
Svg: PersonRunning,
|
||||
description: (
|
||||
<>
|
||||
Cheerio can parse nearly any HTML or XML document. Cheerio works in both
|
||||
browser and server environments.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function Feature({ title, Svg, description }: FeatureItem) {
|
||||
return (
|
||||
<div className="col col--4">
|
||||
<div className="text--center">
|
||||
<Svg height="200" width="200" role="img" />
|
||||
</div>
|
||||
<div className="text--center padding-horiz--md">
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomepageFeatures(): JSX.Element {
|
||||
return (
|
||||
<section className="container padding-top--xl ">
|
||||
<div className="row">
|
||||
{FeatureList.map((props, idx) => (
|
||||
<Feature key={idx} {...props} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
.emphasis {
|
||||
background-color: var(--ifm-color-emphasis-100);
|
||||
}
|
||||
|
||||
.you {
|
||||
border: 1px solid var(--ifm-color-emphasis-400);
|
||||
border-radius: 10px;
|
||||
color: var(--ifm-font-color-base);
|
||||
transition:
|
||||
border 0.2s ease-in-out,
|
||||
background-color 0.2s ease-in-out,
|
||||
color 0.2s ease-in-out,
|
||||
box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.you:hover {
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
background-color: var(--ifm-color-info-lightest);
|
||||
color: var(--ifm-font-color-base);
|
||||
box-shadow: 0 0 0.5rem 0 rgba(150, 0, 0, 0.2);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from 'react';
|
||||
import HeartSvg from '@site/static/img/1F496.svg';
|
||||
import styles from './homepage-sponsors.module.css';
|
||||
|
||||
import Sponsors from '../../sponsors.json';
|
||||
|
||||
export function HeadlineSponsors() {
|
||||
return (
|
||||
<div className={`padding-vert--lg margin-vert--lg ${styles.emphasis}`}>
|
||||
<div className="container">
|
||||
<h2 className="text--center">Supported and Backed by</h2>
|
||||
<div
|
||||
className="container row"
|
||||
style={{ justifyContent: 'space-evenly' }}
|
||||
>
|
||||
{Sponsors.headliner.map((sponsor) => (
|
||||
<div className="col col--2 avatar row row--align-center margin-top--sm">
|
||||
<a
|
||||
className="avatar__photo-link avatar__photo avatar__photo--lg"
|
||||
style={{ borderRadius: '10px' }}
|
||||
href={sponsor.url}
|
||||
key={sponsor.name}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img src={sponsor.image} alt={`${sponsor.name} logo`} />
|
||||
</a>
|
||||
|
||||
<div className="avatar__intro">
|
||||
<div className="avatar__name">{sponsor.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<a
|
||||
className={`col col--2 avatar row padding--md margin-top--sm ${styles.you}`}
|
||||
href="https://github.com/sponsors/cheeriojs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<HeartSvg className="avatar__photo-link avatar__photo avatar__photo--lg" />
|
||||
|
||||
<div className="avatar__intro">
|
||||
<div className="avatar__name">…and you?</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
interface TweetItem {
|
||||
id: string;
|
||||
name: string;
|
||||
user: string;
|
||||
date: string;
|
||||
tweet: React.ReactNode;
|
||||
}
|
||||
|
||||
const TweetList = [
|
||||
{
|
||||
id: '628016191928446977',
|
||||
name: 'Axel Rauschmayer',
|
||||
user: 'rauschma',
|
||||
date: '2015-08-03T02:35:00.000Z',
|
||||
tweet: (
|
||||
<>
|
||||
For transforming HTML via Node.js scripts, @mattmueller's cheerio works
|
||||
really well.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: '1616150822932385792',
|
||||
name: 'Valeri Karpov',
|
||||
user: 'code_barbarian',
|
||||
date: '2023-01-19T19:09:00.000Z',
|
||||
tweet: (
|
||||
<>
|
||||
Cheerio is a weird npm module: most devs have never heard of it, but I
|
||||
rarely build an app without it.
|
||||
<br />
|
||||
<br />
|
||||
So much utility for quick and easy HTML transformations.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: '1545481085865320449',
|
||||
name: 'Thomas Boutell',
|
||||
user: 'boutell',
|
||||
date: '2021-07-08T19:52:00.000Z',
|
||||
tweet: (
|
||||
<>
|
||||
You probably shouldn't use jQuery, but if you're great at jQuery, you're
|
||||
going to be really popular on server-side projects that need web
|
||||
scraping or HTML transformation. "npm install cheerio" ahoy!
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: '552311181760008192',
|
||||
name: 'Alistair G MacDonald',
|
||||
user: 'html5js',
|
||||
date: '2015-01-06T03:50:00.000Z',
|
||||
tweet: (
|
||||
<>
|
||||
Looking for a faster, cleaner alternative to basic JSDOM? Try Cheerio!
|
||||
#npm #javascript #nodejs
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: '1466753169900273667',
|
||||
name: 'Yogini Bende',
|
||||
user: 'hey_yogini',
|
||||
date: '2021-12-03T12:56:00.000Z',
|
||||
tweet: 'Cheerio is 🔥',
|
||||
},
|
||||
{
|
||||
id: '936243649234591744',
|
||||
name: 'Jonny Frodsham',
|
||||
user: 'jonnyfrodsham',
|
||||
date: '2017-11-30T14:40:00.000Z',
|
||||
tweet: (
|
||||
<>
|
||||
Needed to do a quick web scrape in Node for a demo. Seems like I'm back
|
||||
using jQuery in the super timesaving cheerio npm package 😯
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: '264033999272439809',
|
||||
name: 'Thomas Steiner',
|
||||
user: 'tomayac',
|
||||
date: '2012-11-01T15:59:00.000Z',
|
||||
tweet: (
|
||||
<>
|
||||
npm install cheerio. That's the #jQuery DOM API for #nodeJS essentially.
|
||||
Thanks, @MattMueller
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: '1403139379757977602',
|
||||
name: 'Mike Pennisi',
|
||||
user: 'JugglinMike',
|
||||
date: '2021-06-11T04:57:00.000Z',
|
||||
tweet: (
|
||||
<>
|
||||
Thank you @fb55 for tirelessly pushing Cheerio to version 1.0. That
|
||||
library helps so many developers expand their horizons beyond the
|
||||
browser, and you've been making it possible for a decade!
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: '1186972238190403587',
|
||||
name: 'Matthew Phillips',
|
||||
user: 'matthewcp',
|
||||
date: '2019-10-23T12:46:00.000Z',
|
||||
tweet: (
|
||||
<>
|
||||
Cheerio is (still) such a useful tool for manipulating HTML. Shout to
|
||||
@MattMueller for saving me an untold amount of time over the years.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function Tweet({ id, name, user, date, tweet }: TweetItem) {
|
||||
const formattedDate = new Date(date).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card-demo col col--4 margin-vert--sm">
|
||||
<div className="card">
|
||||
<div className="card__header">
|
||||
<div className="avatar">
|
||||
<img
|
||||
className="avatar__photo"
|
||||
src={`https://unavatar.io/${user}`}
|
||||
alt={`${name}'s avatar`}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="avatar__intro">
|
||||
<div className="avatar__name">{name}</div>
|
||||
<small className="avatar__subtitle">@{user}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__body">{tweet}</div>
|
||||
<time className="card__footer">
|
||||
<a
|
||||
href={`https://twitter.com/${user}/status/${id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{formattedDate}
|
||||
</a>
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomepageTweets() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h2 className="text--center">What Our Users Say</h2>
|
||||
<div className="row">
|
||||
{TweetList.map((props) => (
|
||||
<Tweet {...props} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
94
website/src/components/live-code.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
SandpackProvider,
|
||||
SandpackCodeEditor,
|
||||
SandpackConsole,
|
||||
useSandpack,
|
||||
} from '@codesandbox/sandpack-react';
|
||||
|
||||
interface LiveCodeProps {
|
||||
code: string;
|
||||
}
|
||||
|
||||
function ResetButton() {
|
||||
const { sandpack } = useSandpack();
|
||||
|
||||
const handleReset = useCallback(() => sandpack.resetAllFiles(), [sandpack]);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="px-2 py-1 text-xs font-medium text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-200 dark:hover:bg-slate-700 rounded transition-colors"
|
||||
title="Reset code and re-run"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function RunButton() {
|
||||
const { sandpack } = useSandpack();
|
||||
|
||||
const handleRun = () => {
|
||||
const { code } = sandpack.files['/index.js'];
|
||||
sandpack.updateFile('/index.js', code, true);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleRun}
|
||||
className="px-2 py-1 text-xs font-medium text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-200 dark:hover:bg-slate-700 rounded transition-colors"
|
||||
title="Run code"
|
||||
>
|
||||
Run
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Toolbar() {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-slate-100 dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700">
|
||||
<span className="text-xs font-medium text-slate-600 dark:text-slate-400 uppercase tracking-wide">
|
||||
Live Editor
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<RunButton />
|
||||
<ResetButton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LiveCode({ code }: LiveCodeProps) {
|
||||
// Wrap user code to run immediately and output via console.log
|
||||
const wrappedCode = `import * as cheerio from 'cheerio';
|
||||
|
||||
${code}
|
||||
`;
|
||||
|
||||
return (
|
||||
<div className="my-4 overflow-hidden rounded-lg border border-slate-200 dark:border-slate-700 not-prose">
|
||||
<SandpackProvider
|
||||
template="vanilla"
|
||||
theme="auto"
|
||||
files={{
|
||||
'/index.js': wrappedCode,
|
||||
}}
|
||||
customSetup={{
|
||||
dependencies: {
|
||||
cheerio: 'latest',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
<SandpackCodeEditor showLineNumbers style={{ height: '200px' }} />
|
||||
<SandpackConsole
|
||||
style={{ height: '150px' }}
|
||||
standalone
|
||||
showHeader
|
||||
showResetConsoleButton
|
||||
/>
|
||||
</SandpackProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,8 +5,6 @@ authors: fb55
|
||||
tags: [release, announcement]
|
||||
---
|
||||
|
||||
# Cheerio 1.0 Released, Batteries Included 🔋
|
||||
|
||||
Cheerio 1.0 is out! After 12 release candidates and just a short seven years
|
||||
after the initial 1.0 release candidate, it is finally time to call Cheerio 1.0
|
||||
complete. The theme for this release is "batteries included", with common use
|
||||
24
website/src/content/config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
const docs = defineCollection({
|
||||
type: 'content',
|
||||
schema: z.object({
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
sidebar_position: z.number().optional(),
|
||||
sidebar_label: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
const blog = defineCollection({
|
||||
type: 'content',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
slug: z.string().optional(),
|
||||
authors: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
date: z.date().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { docs, blog };
|
||||
@@ -27,7 +27,7 @@ For example, if you want the contents of `<noscript>` tags to be parsed as HTML,
|
||||
you can set the `scriptingEnabled` option to false.
|
||||
|
||||
For a full list of options and their effects, have a look at
|
||||
[the API documentation](/docs/api/interfaces/Parse5Options).
|
||||
[the API documentation](/docs/api/interfaces/CheerioOptions).
|
||||
|
||||
### Fragment Mode
|
||||
|
||||
@@ -16,7 +16,7 @@ document.
|
||||
|
||||
:::
|
||||
|
||||
:::danger Availability of methods
|
||||
:::danger[Availability of methods]
|
||||
|
||||
The `loadBuffer`, `stringStream`, `decodeStream`, and `fromURL` methods are not
|
||||
available in the browser environment. Instead, use the `load` method to parse
|
||||
@@ -57,7 +57,7 @@ $.html();
|
||||
:::
|
||||
|
||||
Learn more about the `load` method in the
|
||||
[API documentation](/docs/api/functions/load).
|
||||
[API documentation](/docs/api/variables/load).
|
||||
|
||||
## `loadBuffer`
|
||||
|
||||
@@ -104,7 +104,7 @@ $('h1').text('Hello, World!');
|
||||
const text = $('p').text();
|
||||
```
|
||||
|
||||
:::tip Note
|
||||
:::tip[Note]
|
||||
|
||||
`text()` returns the `textContent` of all passed elements. The result will
|
||||
include the contents of `<script>` and `<style>` elements. To avoid this, use
|
||||
@@ -54,7 +54,7 @@ const $selected = $('.selected');
|
||||
const $selected = $('[data-selected=true]');
|
||||
```
|
||||
|
||||
:::tip XML Namespaces
|
||||
:::tip[XML Namespaces]
|
||||
|
||||
You can select with XML Namespaces but
|
||||
[due to the CSS specification](https://www.w3.org/TR/2011/REC-css3-selectors-20110929/#attribute-selectors),
|
||||
@@ -3,6 +3,8 @@ sidebar_position: 4
|
||||
description: Traverse the DOM tree and filter elements.
|
||||
---
|
||||
|
||||
import { LiveCode } from '@/components/live-code';
|
||||
|
||||
# Traversing the DOM
|
||||
|
||||
Traversing a document with Cheerio allows you to select and manipulate specific
|
||||
@@ -40,7 +42,7 @@ the current selection.
|
||||
Here's an example of using `find` to select all `<li>` elements within a `<ul>`
|
||||
element:
|
||||
|
||||
```js live noInline
|
||||
```js live
|
||||
const $ = cheerio.load(
|
||||
`<ul>
|
||||
<li>Item 1</li>
|
||||
@@ -49,7 +51,7 @@ const $ = cheerio.load(
|
||||
);
|
||||
|
||||
const listItems = $('ul').find('li');
|
||||
render(<>List item count: {listItems.length}</>);
|
||||
console.log(`List item count: ${listItems.length}`);
|
||||
```
|
||||
|
||||
### `children`
|
||||
@@ -61,7 +63,7 @@ direct children of the current selection.
|
||||
Here's an example of using `children` to select all `<li>` elements within a
|
||||
`<ul>` element:
|
||||
|
||||
```js live noInline
|
||||
```js live
|
||||
const $ = cheerio.load(
|
||||
`<ul>
|
||||
<li>Item 1</li>
|
||||
@@ -70,7 +72,7 @@ const $ = cheerio.load(
|
||||
);
|
||||
|
||||
const listItems = $('ul').children('li');
|
||||
render(<>List item count: {listItems.length}</>);
|
||||
console.log(`List item count: ${listItems.length}`);
|
||||
```
|
||||
|
||||
### `contents`
|
||||
@@ -82,7 +84,7 @@ selection containing all children of the current selection.
|
||||
Here's an example of using `contents` to select all children of a `<div>`
|
||||
element:
|
||||
|
||||
```js live noInline
|
||||
```js live
|
||||
const $ = cheerio.load(
|
||||
`<div>
|
||||
Text <p>Paragraph</p>
|
||||
@@ -90,7 +92,7 @@ const $ = cheerio.load(
|
||||
);
|
||||
|
||||
const contents = $('div').contents();
|
||||
render(<>Contents count: {contents.length}</>);
|
||||
console.log(`Contents count: ${contents.length}`);
|
||||
```
|
||||
|
||||
## Moving Up the DOM Tree
|
||||
@@ -107,7 +109,7 @@ element of each element in the current selection.
|
||||
Here's an example of using `parent` to select the parent `<ul>` element of a
|
||||
`<li>` element:
|
||||
|
||||
```js live noInline
|
||||
```js live
|
||||
const $ = cheerio.load(
|
||||
`<ul>
|
||||
<li>Item 1</li>
|
||||
@@ -115,7 +117,7 @@ const $ = cheerio.load(
|
||||
);
|
||||
|
||||
const list = $('li').parent();
|
||||
render(<>{list.prop('tagName')}</>);
|
||||
console.log(list.prop('tagName'));
|
||||
```
|
||||
|
||||
### `parents` and `parentsUntil`
|
||||
@@ -132,7 +134,7 @@ selection up to (but not including) the specified ancestor.
|
||||
Here's an example of using `parents` and `parentsUntil` to select ancestor
|
||||
elements of a `<li>` element:
|
||||
|
||||
```js live noInline
|
||||
```js live
|
||||
const $ = cheerio.load(
|
||||
`<div>
|
||||
<ul>
|
||||
@@ -144,14 +146,8 @@ const $ = cheerio.load(
|
||||
const ancestors = $('li').parents();
|
||||
const ancestorsUntil = $('li').parentsUntil('div');
|
||||
|
||||
render(
|
||||
<>
|
||||
<p>
|
||||
Ancestor count (also includes "body" and "html" tags): {ancestors.length}
|
||||
</p>
|
||||
<p>Ancestor count (until "div"): {ancestorsUntil.length}</p>
|
||||
</>,
|
||||
);
|
||||
console.log(`Ancestor count (includes body and html): ${ancestors.length}`);
|
||||
console.log(`Ancestor count (until div): ${ancestorsUntil.length}`);
|
||||
```
|
||||
|
||||
### `closest`
|
||||
@@ -164,7 +160,7 @@ matching ancestor is found, the method returns an empty selection.
|
||||
Here's an example of using `closest` to select the closest ancestor `<ul>`
|
||||
element of a `<li>` element:
|
||||
|
||||
```js live noInline
|
||||
```js live
|
||||
const $ = cheerio.load(
|
||||
`<div>
|
||||
<ul>
|
||||
@@ -174,7 +170,7 @@ const $ = cheerio.load(
|
||||
);
|
||||
|
||||
const list = $('li').closest('ul');
|
||||
render(<>{list.prop('tagName')}</>);
|
||||
console.log(list.prop('tagName'));
|
||||
```
|
||||
|
||||
## Moving Sideways Within the DOM Tree
|
||||
@@ -196,7 +192,7 @@ containing the previous sibling element for each element in the given selection.
|
||||
Here's an example of using `next` and `prev` to select sibling elements of a
|
||||
`<li>` element:
|
||||
|
||||
```js live noInline
|
||||
```js live
|
||||
const $ = cheerio.load(
|
||||
`<ul>
|
||||
<li>Item 1</li>
|
||||
@@ -207,12 +203,8 @@ const $ = cheerio.load(
|
||||
const nextItem = $('li:first').next();
|
||||
const prevItem = $('li:eq(1)').prev();
|
||||
|
||||
render(
|
||||
<>
|
||||
<p>Next: {nextItem.text()}</p>
|
||||
<p>Prev: {prevItem.text()}</p>
|
||||
</>,
|
||||
);
|
||||
console.log(`Next: ${nextItem.text()}`);
|
||||
console.log(`Prev: ${prevItem.text()}`);
|
||||
```
|
||||
|
||||
## `nextAll`, `prevAll`, and `siblings`
|
||||
@@ -233,7 +225,7 @@ elements of each element in the current selection.
|
||||
Here's an example of using `nextAll`, `prevAll`, and `siblings` to select
|
||||
sibling elements of a `<li>` element:
|
||||
|
||||
```js live noInline
|
||||
```js live
|
||||
const $ = cheerio.load(
|
||||
`<ul>
|
||||
<li>[1]</li>
|
||||
@@ -246,13 +238,9 @@ const nextAll = $('li:first').nextAll();
|
||||
const prevAll = $('li:last').prevAll();
|
||||
const siblings = $('li:eq(1)').siblings();
|
||||
|
||||
render(
|
||||
<>
|
||||
<p>Next All: {nextAll.text()}</p>
|
||||
<p>Prev All: {prevAll.text()}</p>
|
||||
<p>Siblings: {siblings.text()}</p>
|
||||
</>,
|
||||
);
|
||||
console.log(`Next All: ${nextAll.text()}`);
|
||||
console.log(`Prev All: ${prevAll.text()}`);
|
||||
console.log(`Siblings: ${siblings.text()}`);
|
||||
```
|
||||
|
||||
### `nextUntil` and `prevUntil`
|
||||
@@ -272,7 +260,7 @@ element up to (but not including) the specified element.
|
||||
Here's an example of using `nextUntil` and `prevUntil` to select sibling
|
||||
elements of a `<li>` element:
|
||||
|
||||
```js live noInline
|
||||
```js live
|
||||
const $ = cheerio.load(
|
||||
`<ul>
|
||||
<li>Item 1</li>
|
||||
@@ -284,12 +272,8 @@ const $ = cheerio.load(
|
||||
const nextUntil = $('li:first').nextUntil('li:last-child');
|
||||
const prevUntil = $('li:last').prevUntil('li:first-child');
|
||||
|
||||
render(
|
||||
<>
|
||||
<p>Next: {nextUntil.text()}</p>
|
||||
<p>Prev: {prevUntil.text()}</p>
|
||||
</>,
|
||||
);
|
||||
console.log(`Next: ${nextUntil.text()}`);
|
||||
console.log(`Prev: ${prevUntil.text()}`);
|
||||
```
|
||||
|
||||
## Filtering elements
|
||||
@@ -313,7 +297,7 @@ returns a new selection containing the element at the specified index.
|
||||
Here's an example of using `eq` to select the second `<li>` element within a
|
||||
`<ul>` element:
|
||||
|
||||
```js live noInline
|
||||
```js live
|
||||
const $ = cheerio.load(
|
||||
`<ul>
|
||||
<li>Item 1</li>
|
||||
@@ -322,7 +306,7 @@ const $ = cheerio.load(
|
||||
);
|
||||
|
||||
const secondItem = $('li').eq(1);
|
||||
render(<>{secondItem.text()}</>);
|
||||
console.log(secondItem.text());
|
||||
```
|
||||
|
||||
### `filter` and `not`
|
||||
@@ -339,7 +323,7 @@ elements that do not match the selector.
|
||||
Here's an example of using `filter` and `not` to select `<li>` elements within a
|
||||
`<ul>` element:
|
||||
|
||||
```js live noInline
|
||||
```js live
|
||||
const $ = cheerio.load(
|
||||
`<ul>
|
||||
<li class="item">Item 1</li>
|
||||
@@ -350,12 +334,8 @@ const $ = cheerio.load(
|
||||
const matchingItems = $('li').filter('.item');
|
||||
const nonMatchingItems = $('li').not('.item');
|
||||
|
||||
render(
|
||||
<>
|
||||
<p>Matching: {matchingItems.text()}</p>
|
||||
<p>Non-matching: {nonMatchingItems.text()}</p>
|
||||
</>,
|
||||
);
|
||||
console.log(`Matching: ${matchingItems.text()}`);
|
||||
console.log(`Non-matching: ${nonMatchingItems.text()}`);
|
||||
```
|
||||
|
||||
### `has`
|
||||
@@ -368,7 +348,7 @@ an element matching the selector.
|
||||
Here's an example of using `has` to select `<li>` elements within a `<ul>`
|
||||
element that contain a `<strong>` element:
|
||||
|
||||
```js live noInline
|
||||
```js live
|
||||
const $ = cheerio.load(
|
||||
`<ul>
|
||||
<li>Item 1</li>
|
||||
@@ -379,7 +359,7 @@ const $ = cheerio.load(
|
||||
);
|
||||
|
||||
const matchingItems = $('li').has('strong');
|
||||
render(<>{matchingItems.length}</>);
|
||||
console.log(matchingItems.length);
|
||||
```
|
||||
|
||||
### `first` and `last`
|
||||
@@ -395,7 +375,7 @@ containing the last element.
|
||||
Here's an example of using `first` and `last` to select elements within a `<ul>`
|
||||
element:
|
||||
|
||||
```js live noInline
|
||||
```js live
|
||||
const $ = cheerio.load(
|
||||
`<ul>
|
||||
<li>Item 1</li>
|
||||
@@ -406,12 +386,8 @@ const $ = cheerio.load(
|
||||
const firstItem = $('li').first();
|
||||
const lastItem = $('li').last();
|
||||
|
||||
render(
|
||||
<>
|
||||
<p>First: {firstItem.text()}</p>
|
||||
<p>Last: {lastItem.text()}</p>
|
||||
</>,
|
||||
);
|
||||
console.log(`First: ${firstItem.text()}`);
|
||||
console.log(`Last: ${lastItem.text()}`);
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* Any CSS included here will be global. The classic template
|
||||
* bundles Infima by default. Infima is a CSS framework designed to
|
||||
* work well for content-centric websites.
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(../../static/fonts/inter.woff) format('woff');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
||||
U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Rubik';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(../../static/fonts/rubik.woff) format('woff');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
||||
U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
--ifm-color-primary: #e88c1f;
|
||||
--ifm-color-primary-dark: #d77e16;
|
||||
--ifm-color-primary-darker: #cb7715;
|
||||
--ifm-color-primary-darkest: #a76211;
|
||||
--ifm-color-primary-light: #ea9837;
|
||||
--ifm-color-primary-lighter: #ec9e43;
|
||||
--ifm-color-primary-lightest: #efb167;
|
||||
--ifm-code-font-size: 95%;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||
|
||||
--ifm-font-family-base: 'Rubik', system-ui, -apple-system, 'Segoe UI', Roboto,
|
||||
Ubuntu, Cantarell, 'Noto Sans', sans-serif;
|
||||
--ifm-heading-font-family: 'Inter', var(--ifm-font-family-base);
|
||||
--ifm-heading-font-weight: 600;
|
||||
}
|
||||
|
||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||
[data-theme='dark'] {
|
||||
--ifm-color-primary: #f4a02f;
|
||||
--ifm-color-primary-dark: #f39313;
|
||||
--ifm-color-primary-darker: #eb8c0c;
|
||||
--ifm-color-primary-darkest: #c1730a;
|
||||
--ifm-color-primary-light: #f5ad4b;
|
||||
--ifm-color-primary-lighter: #f6b358;
|
||||
--ifm-color-primary-lightest: #f8c682;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
5
website/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/triple-slash-reference */
|
||||
/* eslint-disable spaced-comment */
|
||||
/* eslint-disable multiline-comment-style */
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
41
website/src/layouts/BaseLayout.astro
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
import '@/styles/global.css';
|
||||
import { ViewTransitions } from 'astro:transitions';
|
||||
import Navbar from '@/components/Navbar.astro';
|
||||
import Footer from '@/components/Footer.astro';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description = 'The fast, flexible & elegant library for parsing and manipulating HTML and XML.',
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content={description} />
|
||||
<meta
|
||||
name="keywords"
|
||||
content="htmlparser, jquery, selector, scraper, parser, dom, xml, html, nodejs"
|
||||
/>
|
||||
<link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
|
||||
<title>{title} | Cheerio</title>
|
||||
<ViewTransitions />
|
||||
</head>
|
||||
<body
|
||||
class="flex min-h-screen flex-col bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100"
|
||||
>
|
||||
<Navbar />
|
||||
<main class="flex-1">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
62
website/src/layouts/BlogLayout.astro
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
import '@/styles/global.css';
|
||||
import Navbar from '@/components/Navbar.astro';
|
||||
import Footer from '@/components/Footer.astro';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
date?: Date;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description = 'The fast, flexible & elegant library for parsing and manipulating HTML and XML.',
|
||||
date,
|
||||
author,
|
||||
} = Astro.props;
|
||||
|
||||
const formattedDate = date
|
||||
? date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
: null;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content={description} />
|
||||
<link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
|
||||
<title>{title} | Cheerio Blog</title>
|
||||
</head>
|
||||
<body
|
||||
class="flex min-h-screen flex-col bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100"
|
||||
>
|
||||
<Navbar />
|
||||
<main class="flex-1">
|
||||
<article class="mx-auto max-w-3xl px-4 py-12">
|
||||
<header class="mb-8">
|
||||
<h1 class="mb-4 text-4xl font-bold">{title}</h1>
|
||||
{
|
||||
(formattedDate || author) && (
|
||||
<div class="flex items-center gap-4 text-slate-500 dark:text-slate-400">
|
||||
{formattedDate && <time>{formattedDate}</time>}
|
||||
{author && <span>by {author}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</header>
|
||||
<div class="prose max-w-none">
|
||||
<slot />
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
57
website/src/layouts/DocsLayout.astro
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
import '@/styles/global.css';
|
||||
import { ViewTransitions } from 'astro:transitions';
|
||||
import Navbar from '@/components/Navbar.astro';
|
||||
import Footer from '@/components/Footer.astro';
|
||||
import Sidebar from '@/components/Sidebar.astro';
|
||||
import TableOfContents from '@/components/TableOfContents.astro';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
headings?: Array<{
|
||||
depth: number;
|
||||
slug: string;
|
||||
text: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description = 'The fast, flexible & elegant library for parsing and manipulating HTML and XML.',
|
||||
headings = [],
|
||||
} = Astro.props;
|
||||
|
||||
const currentPath = Astro.url.pathname;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content={description} />
|
||||
<meta
|
||||
name="keywords"
|
||||
content="htmlparser, jquery, selector, scraper, parser, dom, xml, html, nodejs"
|
||||
/>
|
||||
<link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
|
||||
<title>{title} | Cheerio</title>
|
||||
<ViewTransitions />
|
||||
</head>
|
||||
<body
|
||||
class="flex min-h-screen flex-col bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100"
|
||||
>
|
||||
<Navbar />
|
||||
<div class="mx-auto flex w-full max-w-8xl flex-1">
|
||||
<Sidebar currentPath={currentPath} />
|
||||
<main class="flex-1 min-w-0 px-10 py-10">
|
||||
<article class="prose max-w-none">
|
||||
<slot />
|
||||
</article>
|
||||
</main>
|
||||
<TableOfContents headings={headings} />
|
||||
</div>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
30
website/src/pages/attribution.astro
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Attribution">
|
||||
<div class="mx-auto max-w-3xl px-4 py-12">
|
||||
<h1 class="mb-8 text-4xl font-bold">Attribution</h1>
|
||||
|
||||
<div class="mb-8 flex justify-center gap-8">
|
||||
<img src="/img/1F57A.svg" alt="Dancing man emoji" class="h-32 w-32" />
|
||||
<img src="/img/1F496.svg" alt="Sparkling heart emoji" class="h-32 w-32" />
|
||||
<img src="/img/26A1.svg" alt="Lightning emoji" class="h-32 w-32" />
|
||||
</div>
|
||||
|
||||
<div class="prose mx-auto">
|
||||
<p>
|
||||
Homepage images originate from <a
|
||||
href="https://openmoji.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">OpenMoji</a
|
||||
>, licensed under <a
|
||||
href="https://creativecommons.org/licenses/by-sa/4.0/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">CC BY-SA 4.0</a
|
||||
>. The color of the dancing man emoji was changed to align with the rest
|
||||
of the website.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -1,17 +0,0 @@
|
||||
import DancingMan from '../../static/img/1F57A.svg';
|
||||
import SparklingHeart from '../../static/img/1F496.svg';
|
||||
import Lightning from '../../static/img/26A1.svg';
|
||||
|
||||
# Attribution
|
||||
|
||||
<center>
|
||||
|
||||
<DancingMan height="200px" />
|
||||
<SparklingHeart height="200px" />
|
||||
<Lightning height="200px" />
|
||||
|
||||
</center>
|
||||
|
||||
Homepage images originate from [OpenMoji](https://openmoji.org), licensed under
|
||||
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). The color of
|
||||
the dancing man emoji was changed to align with the rest of the website.
|
||||
33
website/src/pages/blog/[slug].astro
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import BlogLayout from '@/layouts/BlogLayout.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog');
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.id.replace('.md', '').replace('.mdx', '') },
|
||||
props: { post },
|
||||
}));
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { Content } = await render(post);
|
||||
|
||||
// Extract date from filename (e.g., "2024-08-07-version-1.md")
|
||||
const dateParts = post.id.split('-').slice(0, 3);
|
||||
const date = new Date(dateParts.join('-'));
|
||||
|
||||
// Get author name (simplified - in a real app you'd look this up)
|
||||
const authorMap: Record<string, string> = {
|
||||
fb55: 'Felix Boehm',
|
||||
};
|
||||
const authorName = post.data.authors
|
||||
? (Array.isArray(post.data.authors)
|
||||
? post.data.authors.map((a: string) => authorMap[a] || a).join(', ')
|
||||
: authorMap[post.data.authors] || post.data.authors)
|
||||
: undefined;
|
||||
---
|
||||
|
||||
<BlogLayout title={post.data.title} date={date} author={authorName}>
|
||||
<Content />
|
||||
</BlogLayout>
|
||||
118
website/src/pages/blog/index.astro
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import { getCollection, render } from 'astro:content';
|
||||
|
||||
const posts = await getCollection('blog');
|
||||
|
||||
// Sort posts by date (newest first)
|
||||
const sortedPosts = posts.sort((a, b) => {
|
||||
const dateA = new Date(a.id.split('-').slice(0, 3).join('-'));
|
||||
const dateB = new Date(b.id.split('-').slice(0, 3).join('-'));
|
||||
return dateB.getTime() - dateA.getTime();
|
||||
});
|
||||
|
||||
function getPostDate(id: string): string {
|
||||
const datePart = id.split('-').slice(0, 3).join('-');
|
||||
return new Date(datePart).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
function getPostSlug(id: string): string {
|
||||
return id.replace('.md', '').replace('.mdx', '');
|
||||
}
|
||||
|
||||
// Extract excerpt from raw markdown content (content before <!--truncate-->)
|
||||
function getExcerpt(rawContent: string): string | null {
|
||||
const truncateIndex = rawContent.indexOf('<!--truncate-->');
|
||||
if (truncateIndex === -1) return null;
|
||||
|
||||
let excerpt = rawContent.slice(0, truncateIndex).trim();
|
||||
|
||||
// Remove frontmatter
|
||||
const frontmatterEnd = excerpt.indexOf('---', 3);
|
||||
if (excerpt.startsWith('---') && frontmatterEnd !== -1) {
|
||||
excerpt = excerpt.slice(frontmatterEnd + 3).trim();
|
||||
}
|
||||
|
||||
// Remove the first heading (usually the title, which duplicates frontmatter title)
|
||||
excerpt = excerpt.replace(/^#\s+.+\n+/, '').trim();
|
||||
|
||||
// Remove admonition blocks (:::note, :::tip, etc.)
|
||||
excerpt = excerpt.replace(/^:::\w+(\[.*?\])?\n[\s\S]*?^:::\n*/gm, '').trim();
|
||||
|
||||
return excerpt || null;
|
||||
}
|
||||
|
||||
// Pre-render excerpts for each post
|
||||
const postsWithExcerpts = sortedPosts.map((post) => {
|
||||
const excerpt = getExcerpt(post.body || '');
|
||||
return { post, excerpt };
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout title="Blog">
|
||||
<div class="mx-auto max-w-4xl px-4 py-12">
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold">Blog</h1>
|
||||
<p class="mt-2 text-lg text-slate-600 dark:text-slate-400">
|
||||
Updates and announcements from the Cheerio team.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="/blog/rss.xml"
|
||||
class="flex items-center gap-2 rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-600 transition-colors hover:bg-slate-100 dark:border-slate-700 dark:text-slate-400 dark:hover:bg-slate-800"
|
||||
title="RSS Feed"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4">
|
||||
<path d="M3.75 3a.75.75 0 0 0-.75.75v.5c0 .414.336.75.75.75H4c6.075 0 11 4.925 11 11v.25c0 .414.336.75.75.75h.5a.75.75 0 0 0 .75-.75V16C17 8.82 11.18 3 4 3h-.25Z" />
|
||||
<path d="M3 8.75A.75.75 0 0 1 3.75 8H4a8 8 0 0 1 8 8v.25a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1-.75-.75V16a6 6 0 0 0-6-6h-.25A.75.75 0 0 1 3 9.25v-.5ZM7 15a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z" />
|
||||
</svg>
|
||||
RSS
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="space-y-8">
|
||||
{
|
||||
postsWithExcerpts.map(({ post, excerpt }) => (
|
||||
<article class="rounded-lg border border-slate-200 p-6 transition-shadow hover:shadow-lg dark:border-slate-700">
|
||||
<h2 class="mb-2 text-2xl font-semibold">
|
||||
<a
|
||||
href={`/blog/${getPostSlug(post.id)}`}
|
||||
class="text-slate-900 transition-colors hover:text-primary dark:text-white"
|
||||
>
|
||||
{post.data.title}
|
||||
</a>
|
||||
</h2>
|
||||
<div class="mb-4 flex items-center gap-4 text-sm text-slate-500 dark:text-slate-400">
|
||||
<time>{getPostDate(post.id)}</time>
|
||||
{post.data.tags && (
|
||||
<div class="flex gap-2">
|
||||
{post.data.tags.map((tag: string) => (
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-xs dark:bg-slate-800">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{excerpt && (
|
||||
<div class="prose prose-slate dark:prose-invert prose-sm max-w-none mb-4">
|
||||
<p class="text-slate-600 dark:text-slate-400">{excerpt}</p>
|
||||
</div>
|
||||
)}
|
||||
<a
|
||||
href={`/blog/${getPostSlug(post.id)}`}
|
||||
class="text-primary hover:underline font-medium"
|
||||
>
|
||||
Read more →
|
||||
</a>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
39
website/src/pages/blog/rss.xml.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import rss from '@astrojs/rss';
|
||||
import type { APIContext } from 'astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { marked } from 'marked';
|
||||
|
||||
export async function GET(context: APIContext) {
|
||||
const posts = await getCollection('blog');
|
||||
|
||||
// Sort posts by date (newest first)
|
||||
const sortedPosts = posts.toSorted((a, b) => {
|
||||
const dateA = new Date(a.id.split('-').slice(0, 3).join('-'));
|
||||
const dateB = new Date(b.id.split('-').slice(0, 3).join('-'));
|
||||
return dateB.getTime() - dateA.getTime();
|
||||
});
|
||||
|
||||
if (!context.site) {
|
||||
throw new Error('Site URL is required for RSS feed generation');
|
||||
}
|
||||
|
||||
return rss({
|
||||
title: 'Cheerio Blog',
|
||||
description: 'Updates and announcements from the Cheerio team.',
|
||||
site: context.site,
|
||||
items: sortedPosts.map((post) => {
|
||||
const datePart = post.id.split('-').slice(0, 3).join('-');
|
||||
const slug = post.id.replace('.md', '').replace('.mdx', '');
|
||||
|
||||
// Render markdown body to HTML
|
||||
const content = post.body ? marked.parse(post.body) : '';
|
||||
|
||||
return {
|
||||
title: post.data.title,
|
||||
pubDate: new Date(datePart),
|
||||
link: `/blog/${slug}/`,
|
||||
content: content as string,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
22
website/src/pages/docs/[slug].astro
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import DocsLayout from '@/layouts/DocsLayout.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const docs = await getCollection('docs');
|
||||
return docs
|
||||
.filter((doc) => !doc.id.includes('/'))
|
||||
.map((doc) => ({
|
||||
params: { slug: doc.id.replace(/\.mdx?$/, '') },
|
||||
props: { doc },
|
||||
}));
|
||||
}
|
||||
|
||||
const { doc } = Astro.props;
|
||||
const { Content, headings } = await render(doc);
|
||||
const title = doc.data.title || doc.data.sidebar_label || doc.id;
|
||||
---
|
||||
|
||||
<DocsLayout title={title} description={doc.data.description} headings={headings}>
|
||||
<Content />
|
||||
</DocsLayout>
|
||||
22
website/src/pages/docs/advanced/[slug].astro
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import DocsLayout from '@/layouts/DocsLayout.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const docs = await getCollection('docs');
|
||||
return docs
|
||||
.filter((doc) => doc.id.startsWith('advanced/'))
|
||||
.map((doc) => ({
|
||||
params: { slug: doc.id.replace('advanced/', '').replace(/\.mdx?$/, '') },
|
||||
props: { doc },
|
||||
}));
|
||||
}
|
||||
|
||||
const { doc } = Astro.props;
|
||||
const { Content, headings } = await render(doc);
|
||||
const title = doc.data.title || doc.data.sidebar_label || doc.id;
|
||||
---
|
||||
|
||||
<DocsLayout title={title} description={doc.data.description} headings={headings}>
|
||||
<Content />
|
||||
</DocsLayout>
|
||||
30
website/src/pages/docs/api/[...slug].astro
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import DocsLayout from '@/layouts/DocsLayout.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const docs = await getCollection('docs');
|
||||
return docs
|
||||
.filter((doc) => doc.id.startsWith('api/'))
|
||||
.map((doc) => {
|
||||
// Remove 'api/' prefix and file extension
|
||||
let slug = doc.id.replace('api/', '').replace(/\.mdx?$/, '');
|
||||
// Handle index page - should have undefined slug for [...slug] catch-all
|
||||
if (slug === 'index') {
|
||||
slug = undefined;
|
||||
}
|
||||
return {
|
||||
params: { slug },
|
||||
props: { doc },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const { doc } = Astro.props;
|
||||
const { Content, headings } = await render(doc);
|
||||
const title = doc.data.title || doc.data.sidebar_label || 'API Documentation';
|
||||
---
|
||||
|
||||
<DocsLayout title={title} description={doc.data.description} headings={headings}>
|
||||
<Content />
|
||||
</DocsLayout>
|
||||
22
website/src/pages/docs/basics/[slug].astro
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import DocsLayout from '@/layouts/DocsLayout.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const docs = await getCollection('docs');
|
||||
return docs
|
||||
.filter((doc) => doc.id.startsWith('basics/'))
|
||||
.map((doc) => ({
|
||||
params: { slug: doc.id.replace('basics/', '').replace(/\.mdx?$/, '') },
|
||||
props: { doc },
|
||||
}));
|
||||
}
|
||||
|
||||
const { doc } = Astro.props;
|
||||
const { Content, headings } = await render(doc);
|
||||
const title = doc.data.title || doc.data.sidebar_label || doc.id;
|
||||
---
|
||||
|
||||
<DocsLayout title={title} description={doc.data.description} headings={headings}>
|
||||
<Content />
|
||||
</DocsLayout>
|
||||
23
website/src/pages/index.astro
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import Hero from '@/components/Hero.astro';
|
||||
import Features from '@/components/Features.astro';
|
||||
import Sponsors from '@/components/Sponsors.astro';
|
||||
import Testimonials from '@/components/Testimonials.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="The industry standard for working with HTML in JavaScript">
|
||||
<Hero />
|
||||
<Features />
|
||||
<Sponsors />
|
||||
<Testimonials />
|
||||
|
||||
<div class="mx-auto max-w-7xl px-4 py-8">
|
||||
<a
|
||||
href="/docs/intro"
|
||||
class="block rounded-lg bg-primary py-4 text-center text-lg font-semibold text-white transition-colors hover:bg-primary-dark"
|
||||
>
|
||||
Learn more about Cheerio ›
|
||||
</a>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -1,49 +0,0 @@
|
||||
import React from 'react';
|
||||
import Link from '@docusaurus/Link';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import Layout from '@theme/Layout';
|
||||
|
||||
// eslint-disable-next-line n/file-extension-in-import
|
||||
import { HomepageFeatures } from '../components/homepage-features';
|
||||
// eslint-disable-next-line n/file-extension-in-import
|
||||
import { HomepageTweets } from '../components/homepage-tweets';
|
||||
// eslint-disable-next-line n/file-extension-in-import
|
||||
import { HeadlineSponsors } from '../components/homepage-sponsors';
|
||||
|
||||
function HomepageHeader() {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<header className="hero hero--primary padding-vert--xl text--center">
|
||||
<div className="container">
|
||||
<h1 className="hero__title">{siteConfig.title}</h1>
|
||||
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||
<Link className="button button--secondary button--lg" to="/docs/intro">
|
||||
Get Started!
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home(): JSX.Element {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
return (
|
||||
<Layout
|
||||
title="The industry standard for working with HTML in JavaScript"
|
||||
description={siteConfig.tagline}
|
||||
>
|
||||
<HomepageHeader />
|
||||
<HomepageFeatures />
|
||||
<HeadlineSponsors />
|
||||
<HomepageTweets />
|
||||
<div className="container">
|
||||
<Link
|
||||
className="button button--primary button--block margin-vert--lg padding-vert--md"
|
||||
to="/docs/intro"
|
||||
>
|
||||
Learn more about Cheerio ❯
|
||||
</Link>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
31
website/src/plugins/rehype-external-links.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { visit } from 'unist-util-visit';
|
||||
import type { Root, Element } from 'hast';
|
||||
|
||||
function visitExternalLink(node: Element): void {
|
||||
if (
|
||||
node.tagName === 'a' &&
|
||||
node.properties?.href &&
|
||||
typeof node.properties.href === 'string'
|
||||
) {
|
||||
const { href } = node.properties;
|
||||
// Check if it's an external link (starts with http:// or https://)
|
||||
if (href.startsWith('http://') || href.startsWith('https://')) {
|
||||
node.properties.target = '_blank';
|
||||
node.properties.rel = 'noopener noreferrer';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function transformer(tree: Root): void {
|
||||
visit(tree, 'element', visitExternalLink);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rehype plugin to make external links open in a new tab. Adds target="_blank"
|
||||
* and rel="noopener noreferrer" to external links.
|
||||
*
|
||||
* @returns A transformer function.
|
||||
*/
|
||||
export function rehypeExternalLinks() {
|
||||
return transformer;
|
||||
}
|
||||
84
website/src/plugins/remark-admonitions.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { visit } from 'unist-util-visit';
|
||||
import type { Root } from 'mdast';
|
||||
|
||||
interface DirectiveData {
|
||||
hName?: string;
|
||||
hProperties?: Record<string, string>;
|
||||
directiveLabel?: boolean;
|
||||
}
|
||||
|
||||
interface DirectiveNode {
|
||||
type: string;
|
||||
name: string;
|
||||
data?: DirectiveData;
|
||||
children: DirectiveChild[];
|
||||
}
|
||||
|
||||
interface DirectiveChild {
|
||||
type: string;
|
||||
value?: string;
|
||||
data?: DirectiveData;
|
||||
children?: DirectiveChild[];
|
||||
}
|
||||
|
||||
const ADMONITION_TYPES = ['note', 'tip', 'warning', 'danger', 'info'] as const;
|
||||
|
||||
function visitAdmonition(node: unknown): void {
|
||||
const directive = node as DirectiveNode;
|
||||
if (directive.type === 'containerDirective') {
|
||||
if (
|
||||
!ADMONITION_TYPES.includes(
|
||||
directive.name as (typeof ADMONITION_TYPES)[number],
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data: DirectiveData = (directive.data ??= {});
|
||||
|
||||
/*
|
||||
* Get title from the directive label or use default
|
||||
* e.g., :::tip Title Here or just :::tip
|
||||
*/
|
||||
let title =
|
||||
directive.name.charAt(0).toUpperCase() + directive.name.slice(1);
|
||||
|
||||
// Check if there's a custom title in the first text
|
||||
const firstChild = directive.children[0];
|
||||
if (firstChild?.data?.directiveLabel) {
|
||||
title = firstChild.children?.[0]?.value ?? title;
|
||||
// Remove the label paragraph from children
|
||||
directive.children.shift();
|
||||
}
|
||||
|
||||
data.hName = 'div';
|
||||
data.hProperties = {
|
||||
class: `admonition admonition-${directive.name}`,
|
||||
'data-type': directive.name,
|
||||
};
|
||||
|
||||
// Prepend a title element
|
||||
directive.children.unshift({
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
hName: 'p',
|
||||
hProperties: { class: 'admonition-title' },
|
||||
},
|
||||
children: [{ type: 'text', value: title }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function transformer(tree: Root): void {
|
||||
visit(tree, visitAdmonition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remark plugin to transform Docusaurus-style admonitions (:::note, :::tip,
|
||||
* etc.) into custom HTML elements that can be styled with Tailwind.
|
||||
*
|
||||
* @returns A transformer function.
|
||||
*/
|
||||
export function remarkAdmonitions() {
|
||||
return transformer;
|
||||
}
|
||||
23
website/src/plugins/remark-fix-typedoc-links.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { visit } from 'unist-util-visit';
|
||||
import type { Root, Link } from 'mdast';
|
||||
|
||||
function visitTypedocLink(node: Link): void {
|
||||
if (typeof node.url === 'string' && node.url.startsWith('/docs/api/')) {
|
||||
// Remove .md extension from API doc links
|
||||
node.url = node.url.replace(/\.md$/, '');
|
||||
}
|
||||
}
|
||||
|
||||
function transformer(tree: Root): void {
|
||||
visit(tree, 'link', visitTypedocLink);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remark plugin to fix typedoc-generated links. Removes .md extension from
|
||||
* internal API doc links.
|
||||
*
|
||||
* @returns A transformer function.
|
||||
*/
|
||||
export function remarkFixTypedocLinks() {
|
||||
return transformer;
|
||||
}
|
||||
73
website/src/plugins/remark-live-code.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { visit } from 'unist-util-visit';
|
||||
import type { Root, Code, Parent } from 'mdast';
|
||||
|
||||
interface MdxJsxAttribute {
|
||||
type: 'mdxJsxAttribute';
|
||||
name: string;
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
interface MdxJsxFlowElement {
|
||||
type: 'mdxJsxFlowElement';
|
||||
name: string;
|
||||
attributes: MdxJsxAttribute[];
|
||||
children: unknown[];
|
||||
}
|
||||
|
||||
function visitLiveCode(
|
||||
node: Code,
|
||||
index: number | undefined,
|
||||
parent: Parent | undefined,
|
||||
): void {
|
||||
// Check if the code block has 'live' in its meta
|
||||
if (node.meta?.includes('live') && index !== undefined && parent) {
|
||||
// Transform the code node into an MDX JSX element
|
||||
const code = node.value;
|
||||
|
||||
/*
|
||||
* Create an mdxJsxFlowElement node for the LiveCode component
|
||||
* with client:visible for lazy hydration
|
||||
*/
|
||||
const jsxNode: MdxJsxFlowElement = {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'LiveCode',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'code',
|
||||
value: code,
|
||||
},
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'client:visible',
|
||||
value: null,
|
||||
},
|
||||
],
|
||||
children: [],
|
||||
};
|
||||
|
||||
// Replace the code node with the JSX node
|
||||
parent.children.splice(index, 1, jsxNode as unknown as Code);
|
||||
}
|
||||
}
|
||||
|
||||
function transformer(tree: Root): void {
|
||||
visit(tree, 'code', visitLiveCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remark plugin to transform code blocks with 'live' meta into LiveCode
|
||||
* components.
|
||||
*
|
||||
* Usage in markdown:
|
||||
*
|
||||
* ```js
|
||||
* const $ = cheerio.load('<h1>Hello</h1>');
|
||||
* return <>{$('h1').text()}</>;
|
||||
* ```
|
||||
*
|
||||
* @returns A transformer function.
|
||||
*/
|
||||
export function remarkLiveCode() {
|
||||
return transformer;
|
||||
}
|
||||
262
website/src/styles/global.css
Normal file
@@ -0,0 +1,262 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url('/fonts/inter.woff') format('woff');
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
|
||||
U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Rubik';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('/fonts/rubik.woff') format('woff');
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
|
||||
U+FFFD;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-primary: #e88c1f;
|
||||
--color-primary-dark: #d77e16;
|
||||
--color-primary-light: #ea9837;
|
||||
--color-primary-lightest: #efb167;
|
||||
|
||||
--font-sans:
|
||||
'Rubik', system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell,
|
||||
'Noto Sans', sans-serif;
|
||||
--font-heading: 'Inter', var(--font-sans);
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Prose styles for documentation */
|
||||
.prose {
|
||||
max-width: 65ch;
|
||||
}
|
||||
|
||||
.prose h1,
|
||||
.prose h2,
|
||||
.prose h3,
|
||||
.prose h4 {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 600;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.prose h1 {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
|
||||
.prose p {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.prose a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.prose a:hover {
|
||||
color: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.prose code {
|
||||
background-color: #f1f5f9;
|
||||
padding: 0.125rem 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.prose pre {
|
||||
background-color: #1e293b;
|
||||
color: #e2e8f0;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.prose pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.prose ul,
|
||||
.prose ol {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.prose li {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.prose blockquote {
|
||||
border-left: 4px solid var(--color-primary);
|
||||
padding-left: 1rem;
|
||||
margin: 1rem 0;
|
||||
color: #64748b;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Admonition styles */
|
||||
.admonition {
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.admonition-title {
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.5rem 0 !important;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.admonition-note {
|
||||
background-color: #eff6ff;
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
|
||||
.admonition-note .admonition-title {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.admonition-tip {
|
||||
background-color: #f0fdf4;
|
||||
border-left: 4px solid #22c55e;
|
||||
}
|
||||
|
||||
.admonition-tip .admonition-title {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.admonition-info {
|
||||
background-color: #f0f9ff;
|
||||
border-left: 4px solid #0ea5e9;
|
||||
}
|
||||
|
||||
.admonition-info .admonition-title {
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
.admonition-warning {
|
||||
background-color: #fefce8;
|
||||
border-left: 4px solid #eab308;
|
||||
}
|
||||
|
||||
.admonition-warning .admonition-title {
|
||||
color: #a16207;
|
||||
}
|
||||
|
||||
.admonition-danger {
|
||||
background-color: #fef2f2;
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
.admonition-danger .admonition-title {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.admonition p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.admonition code {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-primary: #f4a02f;
|
||||
--color-primary-dark: #f39313;
|
||||
--color-primary-light: #f5ad4b;
|
||||
}
|
||||
|
||||
.prose code {
|
||||
background-color: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.admonition-note {
|
||||
background-color: #1e3a5f;
|
||||
}
|
||||
|
||||
.admonition-note .admonition-title {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.admonition-tip {
|
||||
background-color: #14532d;
|
||||
}
|
||||
|
||||
.admonition-tip .admonition-title {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.admonition-info {
|
||||
background-color: #0c4a6e;
|
||||
}
|
||||
|
||||
.admonition-info .admonition-title {
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
.admonition-warning {
|
||||
background-color: #422006;
|
||||
}
|
||||
|
||||
.admonition-warning .admonition-title {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.admonition-danger {
|
||||
background-color: #450a0a;
|
||||
}
|
||||
|
||||
.admonition-danger .admonition-title {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.admonition code {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
import * as cheerio from '../../../../dist/browser/index.js';
|
||||
|
||||
// Add react-live imports you need here
|
||||
const ReactLiveScope = {
|
||||
cheerio,
|
||||
React,
|
||||
...React,
|
||||
};
|
||||
|
||||
export default ReactLiveScope;
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
// This file is not used in compilation. It is here just for a nice editor experience.
|
||||
"extends": "@tsconfig/docusaurus/tsconfig.json",
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react",
|
||||
"resolveJsonModule": true,
|
||||
"checkJs": true,
|
||||
"strict": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"exclude": [".docusaurus", "build", "node_modules"]
|
||||
"include": ["src", ".astro", "*.json", "*.mjs"]
|
||||
}
|
||||
|
||||
36
website/typedoc.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "https://typedoc.org/schema.json",
|
||||
"entryPoints": ["../src/index.ts"],
|
||||
"tsconfig": "../tsconfig.typedoc.json",
|
||||
"readme": "none",
|
||||
"excludePrivate": true,
|
||||
"plugin": ["typedoc-plugin-markdown", "typedoc-plugin-mdn-links"],
|
||||
"out": "src/content/docs/api",
|
||||
"entryFileName": "index",
|
||||
"router": "kind",
|
||||
"publicPath": "/docs/api/",
|
||||
"fileExtension": ".md",
|
||||
"hidePageHeader": true,
|
||||
"hideBreadcrumbs": true,
|
||||
"useCodeBlocks": true,
|
||||
"expandObjects": true,
|
||||
"parametersFormat": "table",
|
||||
"enumMembersFormat": "table",
|
||||
"typeDeclarationFormat": "table",
|
||||
"indexFormat": "table",
|
||||
"sanitizeComments": true,
|
||||
"externalSymbolLinkMappings": {
|
||||
"domhandler": {
|
||||
"Document": "https://domhandler.js.org/classes/Document.html",
|
||||
"Element": "https://domhandler.js.org/classes/Element.html",
|
||||
"Node": "https://domhandler.js.org/classes/Node.html",
|
||||
"ChildNode": "https://domhandler.js.org/types/ChildNode.html",
|
||||
"ParentNode": "https://domhandler.js.org/types/ParentNode.html",
|
||||
"AnyNode": "https://domhandler.js.org/types/AnyNode.html"
|
||||
},
|
||||
"parse5": {
|
||||
"TreeAdapterTypeMap": "https://parse5.js.org/interfaces/parse5.TreeAdapterTypeMap.html",
|
||||
"TreeAdapter": "https://parse5.js.org/interfaces/parse5.TreeAdapter.html"
|
||||
}
|
||||
}
|
||||
}
|
||||