├── .prettierignore ├── README.md ├── public └── img │ ├── add-dpr.png │ ├── add-dpr_.png │ ├── check-img.png │ ├── add-device.png │ ├── check-fonts.png │ ├── colored-text.png │ ├── devices-list.png │ ├── pixel-glass.png │ ├── switch-dpr.png │ └── devices-button.png ├── .gitignore ├── src ├── components │ ├── siteFooter │ │ ├── siteFooter.module.scss │ │ └── index.tsx │ ├── asideNav │ │ ├── asideNav.module.scss │ │ └── index.tsx │ ├── aside │ │ ├── aside.module.scss │ │ └── index.tsx │ ├── customHead │ │ └── index.tsx │ ├── siteNav │ │ ├── siteNav.module.scss │ │ └── index.tsx │ ├── socials │ │ ├── socials.module.scss │ │ └── index.tsx │ ├── siteHeader │ │ ├── siteHeader.module.scss │ │ └── index.tsx │ ├── article │ │ ├── article.module.scss │ │ └── index.tsx │ ├── layout │ │ ├── layout.module.scss │ │ └── index.tsx │ └── icons │ │ └── index.tsx ├── config.json ├── postsList.ts ├── pages │ ├── _document.tsx │ ├── _app.tsx │ ├── _error.tsx │ ├── 404.tsx │ ├── index.tsx │ └── [slug].tsx ├── types.ts ├── utils │ ├── collectPostsUrls.ts │ ├── markdownToHtml.ts │ └── api.ts └── styles │ ├── themes.scss │ ├── prism.scss │ └── global.scss ├── .editorconfig ├── next-env.d.ts ├── next.config.js ├── .prettierrc.json ├── how-to.md ├── package.json ├── tsconfig.json ├── _posts ├── catching-bug.md ├── index.md ├── drop-of-magic.md ├── first-steps.md ├── check-code.md ├── bem-rules.md ├── accessibility.md └── examples.md ├── .github └── workflows │ └── nextjs.yml └── .stylelintrc /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Подсказки по разметке HTML-документа 2 | 3 | http://yoksel.github.io/easy-markup 4 | -------------------------------------------------------------------------------- /public/img/add-dpr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/easy-markup/HEAD/public/img/add-dpr.png -------------------------------------------------------------------------------- /public/img/add-dpr_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/easy-markup/HEAD/public/img/add-dpr_.png -------------------------------------------------------------------------------- /public/img/check-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/easy-markup/HEAD/public/img/check-img.png -------------------------------------------------------------------------------- /public/img/add-device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/easy-markup/HEAD/public/img/add-device.png -------------------------------------------------------------------------------- /public/img/check-fonts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/easy-markup/HEAD/public/img/check-fonts.png -------------------------------------------------------------------------------- /public/img/colored-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/easy-markup/HEAD/public/img/colored-text.png -------------------------------------------------------------------------------- /public/img/devices-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/easy-markup/HEAD/public/img/devices-list.png -------------------------------------------------------------------------------- /public/img/pixel-glass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/easy-markup/HEAD/public/img/pixel-glass.png -------------------------------------------------------------------------------- /public/img/switch-dpr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/easy-markup/HEAD/public/img/switch-dpr.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | npm-debug.log 3 | .DS_Store 4 | .next 5 | node_modules 6 | tsconfig.tsbuildinfo 7 | out 8 | -------------------------------------------------------------------------------- /public/img/devices-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoksel/easy-markup/HEAD/public/img/devices-button.png -------------------------------------------------------------------------------- /src/components/siteFooter/siteFooter.module.scss: -------------------------------------------------------------------------------- 1 | .siteFooter { 2 | margin-top: 3rem; 3 | padding-top: 1.5rem; 4 | border-top: 3px solid var(--color-border); 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /src/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Простые правила разметки", 3 | "description": "Простые правила разметки HTML-документов, рекомендации по выбору HTML-элементов, примеры кода", 4 | "baseurl": "/easy-markup" 5 | } 6 | -------------------------------------------------------------------------------- /src/postsList.ts: -------------------------------------------------------------------------------- 1 | /* npm run getPosts */ 2 | 3 | const postList = ['/accessibility', '/bem-rules', '/catching-bug', '/check-code', '/drop-of-magic', '/examples', '/first-steps', '/index']; 4 | 5 | export default postList; -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('next').NextConfig} 3 | */ 4 | const nextConfig = { 5 | output: 'export', 6 | basePath: '/easy-markup', 7 | trailingSlash: true, 8 | }; 9 | 10 | module.exports = nextConfig; 11 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | 3 | import '../styles/global.scss'; 4 | import '../styles/prism.scss'; 5 | 6 | // This default export is required in a new `pages/_app.js` file. 7 | export default function MyApp({ Component, pageProps }: AppProps) { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // Not using global because issues with types in node script 2 | 3 | export interface Post { 4 | slug?: string | null; 5 | title?: string | null; 6 | content?: string; 7 | order?: number; 8 | links?: PageUrl[]; 9 | additional_links?: PageUrl[]; 10 | } 11 | 12 | export interface PageUrl { 13 | url: string; 14 | text: string; 15 | } 16 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSameLine": false, 4 | "bracketSpacing": true, 5 | "semi": true, 6 | "singleQuote": true, 7 | "jsxSingleQuote": true, 8 | "trailingComma": "all", 9 | "singleAttributePerLine": true, 10 | "htmlWhitespaceSensitivity": "css", 11 | "printWidth": 80, 12 | "parser": "typescript", 13 | "tabWidth": 2, 14 | "useTabs": false 15 | } 16 | -------------------------------------------------------------------------------- /src/components/asideNav/asideNav.module.scss: -------------------------------------------------------------------------------- 1 | .asideNav { 2 | @media (min-width: 1000px) { 3 | margin-bottom: 2rem; 4 | } 5 | } 6 | 7 | .asideNav__title { 8 | font-weight: bold; 9 | font-size: 1.25rem; 10 | } 11 | 12 | .asideNav__list { 13 | list-style-type: decimal; 14 | font-size: 0.9em; 15 | } 16 | 17 | .asideNav__itemCurrent { 18 | A { 19 | font-weight: bold; 20 | text-decoration: none; 21 | color: inherit; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/aside/aside.module.scss: -------------------------------------------------------------------------------- 1 | .pageNav { 2 | @media (min-width: 1000px) { 3 | margin-bottom: 2rem; 4 | } 5 | } 6 | 7 | .pageNav__title { 8 | font-weight: bold; 9 | font-size: 1.25rem; 10 | } 11 | .pageNav__list { 12 | font-size: 0.9em; 13 | } 14 | OL.pageNav__list { 15 | list-style-type: decimal; 16 | } 17 | 18 | .pageNav__itemCurrent { 19 | A { 20 | font-weight: bold; 21 | text-decoration: none; 22 | color: inherit; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/collectPostsUrls.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { getPostSlugs } from './api'; 3 | 4 | const collectPostsUrls = () => { 5 | const slugs = getPostSlugs().map((slug) => `'/${slug.replace(/\.md$/, '')}'`); 6 | const content = `/* npm run getPosts */\n\nconst postList = [${slugs.join(', ')}]; 7 | 8 | export default postList;`; 9 | 10 | fs.writeFileSync('src/postsList.ts', content); 11 | }; 12 | 13 | collectPostsUrls(); 14 | 15 | console.log(collectPostsUrls()); 16 | -------------------------------------------------------------------------------- /src/components/siteFooter/index.tsx: -------------------------------------------------------------------------------- 1 | import { Post } from '../../types'; 2 | import SiteNav from '../siteNav'; 3 | import styles from './siteFooter.module.scss'; 4 | 5 | const SiteFooter = ({ slug, allPosts }: { slug: string; allPosts: Post[] }) => { 6 | return ( 7 |
11 | 16 |
17 | ); 18 | }; 19 | 20 | export default SiteFooter; 21 | -------------------------------------------------------------------------------- /src/components/customHead/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import config from '../../config.json'; 3 | 4 | interface CustomHeadProps { 5 | title?: string | null; 6 | } 7 | 8 | const CustomHead = ({ title }: CustomHeadProps) => { 9 | const postTitle = title ? `${title} • ` : ''; 10 | const fullTitle = `${postTitle}${config.title}`; 11 | return ( 12 | 13 | {fullTitle} 14 | 18 | 19 | ); 20 | }; 21 | 22 | export default CustomHead; 23 | -------------------------------------------------------------------------------- /src/components/siteNav/siteNav.module.scss: -------------------------------------------------------------------------------- 1 | .siteNav { 2 | @media (min-width: 600px) { 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: center; 6 | } 7 | } 8 | 9 | .siteNav__list { 10 | display: flex; 11 | flex-wrap: wrap; 12 | 13 | margin: 0; 14 | padding: 0; 15 | list-style-type: none; 16 | line-height: 2; 17 | 18 | @media (max-width: 600px) { 19 | margin-bottom: 1.5rem; 20 | } 21 | } 22 | 23 | .siteNav__item { 24 | margin: 0 2rem 0 0; 25 | padding: 0; 26 | } 27 | 28 | .siteNav__itemCurrent A { 29 | text-decoration: none; 30 | font-weight: bold; 31 | color: inherit; 32 | } 33 | -------------------------------------------------------------------------------- /src/pages/_error.tsx: -------------------------------------------------------------------------------- 1 | import { NextApiResponse } from 'next'; 2 | import Layout from '../components/layout'; 3 | 4 | interface ErrorProps { 5 | statusCode?: string; 6 | } 7 | 8 | function Error({ statusCode }: ErrorProps) { 9 | return ( 10 | 11 |

Не удалось отобразить страницу

12 |

13 | Вернуться на главную 14 |

15 |
16 | ); 17 | } 18 | 19 | Error.getInitialProps = ({ 20 | res, 21 | err, 22 | }: { 23 | res: NextApiResponse; 24 | err: Error & { statusCode?: string }; 25 | }) => { 26 | const statusCode = res ? res.statusCode : err ? err?.statusCode : 404; 27 | return { statusCode }; 28 | }; 29 | 30 | export default Error; 31 | -------------------------------------------------------------------------------- /src/components/socials/socials.module.scss: -------------------------------------------------------------------------------- 1 | .socials { 2 | display: flex; 3 | list-style-type: none; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | .socials__item { 9 | margin-right: 1rem; 10 | margin-bottom: 0; 11 | } 12 | 13 | .socials__icon { 14 | transition: opacity 0.25s; 15 | 16 | &:hover { 17 | opacity: .8; 18 | } 19 | } 20 | 21 | .socials__icon--github { 22 | color: var(--color-text); 23 | } 24 | 25 | .socials__icon--twitter { 26 | color: var(--color-link-twitter); 27 | } 28 | 29 | .socials__icon svg { 30 | display: block; 31 | width: 1.4em; 32 | height: 1.4em; 33 | 34 | @media (min-width: 800px) { 35 | width: 1.2em; 36 | height: 1.2em; 37 | } 38 | } 39 | 40 | .socials__icon path { 41 | fill: currentColor; 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import type { GetStaticProps } from 'next'; 2 | import Layout from '../components/layout'; 3 | import { getAllPosts } from '../utils/api'; 4 | import { Post } from '../types'; 5 | 6 | export const getStaticProps = (() => { 7 | const allPosts = getAllPosts(['title', 'slug', 'order']); 8 | return { props: { allPosts } }; 9 | }) satisfies GetStaticProps<{ 10 | allPosts: Post[]; 11 | }>; 12 | 13 | function Page404({ allPosts }: { allPosts: Post[] }) { 14 | return ( 15 | 19 |

404

20 |

Страница не найдена

21 |

22 | Вернуться на главную 23 |

24 |
25 | ); 26 | } 27 | 28 | export default Page404; 29 | -------------------------------------------------------------------------------- /how-to.md: -------------------------------------------------------------------------------- 1 | # Some migration tips 2 | 3 | ## NextJs useful links 4 | 5 | * https://nextjs.org/docs — to start with Nextjs 6 | * https://github.com/vercel/next.js/tree/canary/examples/blog-starter — to add markdown support 7 | 8 | ## Code highlighting 9 | 10 | https://prismjs.com/index.html 11 | 12 | ## Collect posts data 13 | 14 | ``` 15 | npm run getPosts 16 | ``` 17 | 18 | List will be saved to `src/postsList.ts`. In case of adding/removing files command should be runned by hands. 19 | 20 | ## Deploy 21 | 22 | Using github deployments 23 | 24 | * https://github.com/vercel/next.js/issues/57038 — problem with publishing static version of site while using App router (not resolved yet) 25 | * [Branch "x" is not allowed to deploy to github-pages due to environment protection rules.](https://github.com/orgs/community/discussions/39054) 26 | -------------------------------------------------------------------------------- /src/components/siteHeader/siteHeader.module.scss: -------------------------------------------------------------------------------- 1 | .siteHeader { 2 | margin-bottom: 2em; 3 | padding-bottom: 1.5rem; 4 | border-bottom: 3px solid var(--color-border); 5 | } 6 | 7 | .siteHeader__titleWrapper { 8 | position: relative; 9 | } 10 | 11 | .siteHeader__title { 12 | margin: 0.5em 0; 13 | font-size: 2rem; 14 | line-height: 1.4; 15 | font-weight: bold; 16 | 17 | @media (min-width: 800px) { 18 | font-size: 3.5rem; 19 | line-height: 1.5; 20 | } 21 | 22 | A, 23 | A:visited { 24 | text-decoration: none; 25 | color: inherit; 26 | } 27 | 28 | A:hover { 29 | text-decoration: underline; 30 | } 31 | } 32 | 33 | .siteHeader__skipToMainLink { 34 | position: absolute; 35 | top: 100%; 36 | opacity: 0; 37 | background-color: var(--color-background); 38 | 39 | &:focus { 40 | opacity: 1; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "y", 3 | "version": "1.0.0", 4 | "description": "http://yoksel.github.io/easy-markup", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "format": "prettier './app/**/*.ts*' --write", 12 | "getPosts": "ts-node src/utils/collectPostsUrls.ts" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "classnames": "^2.3.2", 18 | "gray-matter": "^4.0.3", 19 | "next": "^13.0.0", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "sass": "^1.69.5" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "20.10.4", 26 | "@types/prismjs": "^1.26.3", 27 | "@types/react": "18.2.45", 28 | "prettier": "^3.1.1", 29 | "prismjs": "^1.29.0", 30 | "typescript": "5.3.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/styles/themes.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-text: #222; 3 | --color-link: hsl(214, 76%, 53%); 4 | --color-link-visited: hsl(214, 76%, 38%); 5 | --color-link-hover: hsl(214, 76%, 67%); 6 | --color-link-twitter: hsl(203, 89%, 53%); 7 | 8 | --color-anchor-text: #ddd; 9 | 10 | --color-background: #fff; 11 | --color-border: #eee; 12 | 13 | --color-image-border: #ddd; 14 | 15 | --color-code-border: #ddd; 16 | --color-code-background: #eee; 17 | } 18 | 19 | @media (prefers-color-scheme: dark) { 20 | :root { 21 | --color-text: #fff; 22 | --color-link: hsl(214, 90%, 68%); 23 | --color-link-visited: hsl(214, 90%, 78%); 24 | --color-link-hover: hsl(214, 90%, 90%); 25 | 26 | --color-anchor-text: #555; 27 | 28 | --color-background: #222; 29 | --color-border: #333; 30 | 31 | --color-image-border: #555; 32 | 33 | --color-code-border: #555; 34 | --color-code-background: #333; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": false, 11 | "noEmit": true, 12 | "noImplicitAny": true, 13 | "incremental": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "strictNullChecks": true 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | ".next/types/**/*.ts", 30 | "src/**/*.tsx", 31 | "src/**/*.ts", 32 | "src/global.ts", 33 | ], 34 | "exclude": [ 35 | "node_modules", 36 | ".next" 37 | ], 38 | "ts-node": { 39 | // these options are overrides used only by ts-node 40 | // same as the --compilerOptions flag and the TS_NODE_COMPILER_OPTIONS environment variable 41 | "compilerOptions": { 42 | "module": "NodeNext" 43 | }, 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /src/components/article/article.module.scss: -------------------------------------------------------------------------------- 1 | .article__title { 2 | margin-top: 0; 3 | font-size: 1.8rem; 4 | 5 | @media (min-width: 800px) { 6 | font-size: 2.5rem; 7 | } 8 | } 9 | 10 | .article H2, 11 | .article H3, 12 | .article H4, 13 | .article H5 { 14 | A { 15 | color: inherit; 16 | text-decoration: none; 17 | 18 | &::after { 19 | content: '#'; 20 | // otherwise screen reader reads # 21 | visibility: hidden; 22 | margin-left: 0.2em; 23 | color: var(--color-anchor-text); 24 | } 25 | 26 | &:hover::after { 27 | visibility: visible; 28 | color: var(--color-link); 29 | } 30 | } 31 | } 32 | 33 | .article H2 { 34 | font-size: 1.5rem; 35 | 36 | @media (min-width: 800px) { 37 | font-size: 2rem; 38 | } 39 | 40 | SUP { 41 | font-size: 0.7rem; 42 | opacity: 0.5; 43 | 44 | &:hover { 45 | opacity: 1; 46 | } 47 | } 48 | } 49 | 50 | .article H3 { 51 | font-size: 1.2rem; 52 | 53 | @media (min-width: 800px) { 54 | font-size: 1.5rem; 55 | } 56 | } 57 | 58 | .article H4 { 59 | font-size: 1.25rem; 60 | } 61 | -------------------------------------------------------------------------------- /src/components/asideNav/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useId } from 'react'; 3 | import styles from './asideNav.module.scss'; 4 | import { PageUrl } from '../../types'; 5 | 6 | interface AsideNavProps { 7 | title: string; 8 | links?: PageUrl[]; 9 | } 10 | 11 | const AsideNav = ({ title, links }: AsideNavProps) => { 12 | // id for every aside nav should be unique 13 | const id = useId(); 14 | 15 | if (!links?.length) return null; 16 | 17 | return ( 18 | 41 | ); 42 | }; 43 | 44 | export default AsideNav; 45 | -------------------------------------------------------------------------------- /src/components/siteHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import { Post } from '../../types'; 2 | import SiteNav from '../siteNav'; 3 | import styles from './siteHeader.module.scss'; 4 | 5 | const SiteHeader = ({ slug, allPosts }: { slug: string; allPosts: Post[] }) => { 6 | const titleText = 'Простые правила разметки'; 7 | const isHomepage = slug === 'index'; 8 | const titleContent = isHomepage ? titleText : {titleText}; 9 | const Element = isHomepage ? 'h1' : 'div'; 10 | 11 | return ( 12 |
16 | 25 | 26 | 31 |
32 | ); 33 | }; 34 | 35 | export default SiteHeader; 36 | -------------------------------------------------------------------------------- /src/components/layout/layout.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | min-width: 300px; 3 | max-width: 1200px; 4 | margin: 0 auto; 5 | padding: 0 2rem 3rem; 6 | box-sizing: border-box; 7 | } 8 | 9 | .main { 10 | min-height: 200px; 11 | } 12 | 13 | .siteContent::after { 14 | content: ""; 15 | display: block; 16 | width: 100%; 17 | clear: both; 18 | } 19 | 20 | @media (max-width: 1000px) { 21 | .siteContent { 22 | display: flex; 23 | flex-direction: column; 24 | } 25 | 26 | .main { 27 | order: 1; 28 | } 29 | 30 | .aside { 31 | margin-bottom: 2rem; 32 | } 33 | 34 | .page--index { 35 | .main { 36 | order: 0; 37 | margin-bottom: 2rem; 38 | } 39 | .aside { 40 | margin-bottom: 0; 41 | } 42 | } 43 | } 44 | 45 | @media (min-width: 1000px) { 46 | .main { 47 | // to enable sticky sidebar 48 | float: left; 49 | width: 100%; 50 | padding-right: 320px; 51 | box-sizing: border-box; 52 | } 53 | 54 | .aside { 55 | float: right; 56 | margin-left: -300px; 57 | width: 280px; 58 | padding-top: 1rem; 59 | 60 | position: sticky; 61 | top: 20px; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/article/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useRef } from 'react'; 4 | import styles from './article.module.scss'; 5 | 6 | interface ArticleProps { 7 | title?: string | null; 8 | content: string; 9 | } 10 | 11 | const Article = ({ title, content }: ArticleProps) => { 12 | const heading = useRef(null); 13 | 14 | useEffect(() => { 15 | // Work on static version on second visited page but for all rerenders 16 | heading?.current?.focus(); 17 | 18 | if (typeof window !== 'undefined') { 19 | // https://blog.codepen.io/documentation/embedded-pens/#delayed-embeds 20 | (window as any).__CPEmbed?.(); 21 | } 22 | }); 23 | 24 | return ( 25 |
29 | {title && ( 30 |

36 | {title} 37 |

38 | )} 39 |
40 |
41 | ); 42 | }; 43 | 44 | export default Article; 45 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { InferGetStaticPropsType, GetStaticProps } from 'next'; 3 | import { getAllPosts, getPostBySlug } from '../utils/api'; 4 | import markdownToHtml from '../utils/markdownToHtml'; 5 | import Article from '../components/article'; 6 | import Layout from '../components/layout'; 7 | import { Post } from '../types'; 8 | 9 | export const getStaticProps = (async () => { 10 | const post = getPostBySlug({ 11 | slug: 'index', 12 | fields: ['title', 'slug', 'content', 'links', 'additional_links'], 13 | }); 14 | 15 | if (!post) { 16 | throw new Error('Post not found'); 17 | } 18 | 19 | const allPosts = getAllPosts(['title', 'slug', 'order']); 20 | 21 | return { props: { post, allPosts } }; 22 | }) satisfies GetStaticProps<{ 23 | post: Post; 24 | allPosts: Post[]; 25 | }>; 26 | 27 | export default function Page({ 28 | post, 29 | allPosts, 30 | }: InferGetStaticPropsType) { 31 | const content = markdownToHtml(post?.content || ''); 32 | 33 | return ( 34 | 39 |
40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/markdownToHtml.ts: -------------------------------------------------------------------------------- 1 | import Prism from 'prismjs'; 2 | 3 | function addHighlighting(_: unknown, type: string, code: string) { 4 | const cleanCode = code.replace(/^\n/, ''); 5 | const highlightedCode = Prism.highlight( 6 | cleanCode, 7 | Prism.languages[type], 8 | 'type', 9 | ); 10 | return `
${highlightedCode}
`; 11 | } 12 | 13 | function addAnchors(_: unknown, level: string, id: string, text: string) { 14 | const openTag = ``; 15 | const closeTag = ``; 16 | const textInLink = `${text}`; 17 | return `${openTag}${textInLink}${closeTag}`; 18 | } 19 | 20 | export default function markdownToHtml(markdown: string): string { 21 | // Add markup and highlighting to code blocks 22 | const withCodeBlocks = markdown.replace( 23 | /```(?html|css|js)(?[^`]+)```/gm, 24 | addHighlighting, 25 | ); 26 | 27 | // Wrap text in titles with link to enable anchors 28 | const withAnchors = withCodeBlocks.replace( 29 | /[2-5]) id="(?[^"]+)[^>]+>(?[^\<]+)(.*)<\/h[2-5]>/gm, 30 | addAnchors, 31 | ); 32 | return withAnchors; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/aside/index.tsx: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames'; 2 | import styles from './aside.module.scss'; 3 | import AsideNav from '../asideNav'; 4 | import { Post } from '../../types'; 5 | 6 | const projects = [ 7 | { 8 | text: 'Простые правила разметки', 9 | url: '/', 10 | }, 11 | { 12 | text: 'Простой CSS', 13 | url: 'https://yoksel.github.io/easy-css/', 14 | }, 15 | { 16 | text: 'HTML & CSS: как не надо', 17 | url: 'https://yoksel.github.io/bad-practices/', 18 | }, 19 | ]; 20 | 21 | const Aside = ({ 22 | className, 23 | slug, 24 | post, 25 | }: { 26 | className: string; 27 | slug: string; 28 | post: Post; 29 | }) => { 30 | const { links, additional_links } = post; 31 | 32 | return ( 33 | 56 | ); 57 | }; 58 | 59 | export default Aside; 60 | -------------------------------------------------------------------------------- /src/components/socials/index.tsx: -------------------------------------------------------------------------------- 1 | import { PageUrl } from '../../types'; 2 | import { GithubIcon, TwitterIcon } from '../icons'; 3 | import styles from './socials.module.scss'; 4 | import classnames from 'classnames'; 5 | 6 | interface SocialUrl extends PageUrl { 7 | type: 'github' | 'twitter'; 8 | icon: React.ReactNode; 9 | } 10 | 11 | const items: SocialUrl[] = [ 12 | { 13 | url: 'https://github.com/yoksel/easy-markup/', 14 | text: 'Github', 15 | type: 'github', 16 | icon: , 17 | }, 18 | { 19 | url: 'https://twitter.com/yoksel', 20 | text: 'Twitter', 21 | type: 'twitter', 22 | icon: , 23 | }, 24 | ]; 25 | 26 | const Socials = () => ( 27 | 50 | ); 51 | 52 | export default Socials; 53 | -------------------------------------------------------------------------------- /src/pages/[slug].tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { 3 | InferGetStaticPropsType, 4 | GetStaticProps, 5 | GetStaticPaths, 6 | } from 'next'; 7 | import { getAllPosts, getPostBySlug } from '../utils/api'; 8 | import markdownToHtml from '../utils/markdownToHtml'; 9 | import Article from '../components/article'; 10 | import Layout from '../components/layout'; 11 | import postsList from '../postsList'; 12 | import { Post } from '../types'; 13 | 14 | export const getStaticPaths = (async () => { 15 | return { 16 | // List of all paths which should be created during the build 17 | paths: postsList, 18 | fallback: false, // false or "blocking" 19 | }; 20 | }) satisfies GetStaticPaths; 21 | 22 | export const getStaticProps = (async (context) => { 23 | const { slug } = context?.params || {}; 24 | 25 | if (!slug || Array.isArray(slug)) { 26 | throw new Error('No page path'); 27 | } 28 | 29 | const post = getPostBySlug({ 30 | slug, 31 | fields: ['title', 'slug', 'content', 'links', 'additional_links'], 32 | }); 33 | 34 | if (!post) { 35 | throw new Error('Post not found'); 36 | } 37 | 38 | const allPosts = getAllPosts(['title', 'slug', 'order']); 39 | return { props: { post, allPosts } }; 40 | }) satisfies GetStaticProps<{ 41 | post: Post; 42 | allPosts: Post[]; 43 | }>; 44 | 45 | export default function Page({ 46 | post, 47 | allPosts, 48 | }: InferGetStaticPropsType) { 49 | const content = markdownToHtml(post?.content || ''); 50 | 51 | return ( 52 | 57 |
61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/api.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vercel/next.js/blob/canary/examples/blog-starter/lib/api.ts 2 | 3 | import fs from 'fs'; 4 | import { join } from 'path'; 5 | import matter from 'gray-matter'; 6 | import { Post } from '../types'; 7 | 8 | const postsDirectory = join(process.cwd(), '_posts'); 9 | 10 | export function getPostSlugs(): string[] { 11 | return fs.readdirSync(postsDirectory); 12 | } 13 | 14 | type Fields = (keyof Post)[]; 15 | 16 | interface GetPostBySlugArgs { 17 | slug: string; 18 | fields: Fields; 19 | } 20 | 21 | export function getPostBySlug(args: GetPostBySlugArgs): Post { 22 | const { slug, fields = [] } = args; 23 | 24 | const realSlug = slug.replace(/\.md$/, ''); 25 | const fullPath = join(postsDirectory, `${realSlug}.md`); 26 | const fileContents = fs.readFileSync(fullPath, 'utf8'); 27 | const { data, content } = matter(fileContents); 28 | const dataTyped: Post = data; 29 | 30 | const filteredFields: { [key: string]: unknown } = {}; 31 | 32 | // Ensure only the minimal needed data is exposed 33 | fields.forEach((field) => { 34 | if (field === 'slug') { 35 | filteredFields[field] = realSlug; 36 | } 37 | if (field === 'content') { 38 | filteredFields[field] = content; 39 | } 40 | 41 | if (data[field] !== undefined) { 42 | filteredFields[field] = dataTyped[field]; 43 | } 44 | }); 45 | 46 | return filteredFields; 47 | } 48 | 49 | export function getAllPosts(fields: Fields = []) { 50 | const slugs = getPostSlugs(); 51 | const posts = slugs 52 | .map((slug) => getPostBySlug({ slug, fields })) 53 | // sort posts by order in ascending order 54 | .sort((post1, post2) => { 55 | if (post1.order && post2.order) { 56 | return post1.order - post2.order; 57 | } 58 | 59 | return 0; 60 | }); 61 | return posts; 62 | } 63 | -------------------------------------------------------------------------------- /src/components/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import Aside from '../aside'; 2 | import SiteFooter from '../siteFooter'; 3 | import SiteHeader from '../siteHeader'; 4 | import styles from './layout.module.scss'; 5 | import { Post } from '../../types'; 6 | import CustomHead from '../customHead'; 7 | import { useEffect } from 'react'; 8 | 9 | interface LayoutProps extends React.PropsWithChildren { 10 | slug: string; 11 | post?: Post; 12 | allPosts?: Post[]; 13 | } 14 | 15 | interface GetPageTitleArgs { 16 | title?: string | null; 17 | slug: string; 18 | } 19 | 20 | const getPageTitle = ({ title, slug }: GetPageTitleArgs) => { 21 | if (title) return title; 22 | 23 | return slug === '404' ? 'Страница не найдена' : undefined; 24 | }; 25 | 26 | const Layout = ({ children, slug, post, allPosts }: LayoutProps) => { 27 | useEffect(() => { 28 | window.addEventListener('keydown', (event) => { 29 | if (event.code !== 'Tab') return; 30 | 31 | document.body.classList.add('keyboard-navigation'); 32 | }); 33 | }); 34 | 35 | return ( 36 |
37 | 38 | {allPosts && ( 39 | 43 | )} 44 |
45 |
49 | {children} 50 |
51 | {post && ( 52 |
59 | {allPosts && ( 60 | 64 | )} 65 |
66 | ); 67 | }; 68 | 69 | export default Layout; 70 | -------------------------------------------------------------------------------- /src/components/icons/index.tsx: -------------------------------------------------------------------------------- 1 | export const TwitterIcon = () => ( 2 | 13 | ); 14 | 15 | export const GithubIcon = () => ( 16 | 27 | ); 28 | -------------------------------------------------------------------------------- /src/components/siteNav/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | // import { getAllPosts } from '../../app/utils/api'; 3 | import Socials from '../socials'; 4 | import styles from './siteNav.module.scss'; 5 | import classnames from 'classnames'; 6 | import { PageUrl, Post } from '../../types'; 7 | 8 | interface SiteNavProps { 9 | slug: string; 10 | ariaLabel: string; 11 | allPosts: Post[]; 12 | } 13 | 14 | const sortByOrder = (post1: Post, post2: Post) => { 15 | if (post1.order !== undefined && post2.order !== undefined) { 16 | return post1.order - post2.order; 17 | } 18 | 19 | return 0; 20 | }; 21 | 22 | const getPageUrls = (allPosts: Post[]): PageUrl[] => { 23 | const allPostsFiltered = allPosts.filter(({ title, slug }) => title && slug); 24 | allPostsFiltered.sort(sortByOrder); 25 | 26 | return allPostsFiltered.map((item) => { 27 | return { 28 | text: item.title!, 29 | url: item.slug!, 30 | }; 31 | }); 32 | }; 33 | 34 | const getUrl = (url: string) => { 35 | const isHomePageLink = url === 'index'; 36 | 37 | return isHomePageLink ? '/' : url; 38 | }; 39 | 40 | const SiteNav = ({ slug, ariaLabel, allPosts }: SiteNavProps) => { 41 | const pageUrls = getPageUrls(allPosts); 42 | 43 | return ( 44 | 74 | ); 75 | }; 76 | 77 | export default SiteNav; 78 | -------------------------------------------------------------------------------- /src/styles/prism.scss: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+css+clike+javascript */ 3 | code[class*="language-"], 4 | pre[class*="language-"] { 5 | color: #f8f8f2; 6 | background: 0 0; 7 | text-shadow: 0 1px rgba(0, 0, 0, 0.3); 8 | font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; 9 | font-size: 1em; 10 | text-align: left; 11 | white-space: pre; 12 | word-spacing: normal; 13 | word-break: normal; 14 | word-wrap: normal; 15 | line-height: 1.5; 16 | -moz-tab-size: 4; 17 | -o-tab-size: 4; 18 | tab-size: 4; 19 | -webkit-hyphens: none; 20 | -moz-hyphens: none; 21 | -ms-hyphens: none; 22 | hyphens: none; 23 | } 24 | pre[class*="language-"] { 25 | padding: 1em; 26 | margin: 0.5em 0; 27 | overflow: auto; 28 | border-radius: 0.3em; 29 | } 30 | :not(pre) > code[class*="language-"], 31 | pre[class*="language-"] { 32 | background: #272822; 33 | } 34 | :not(pre) > code[class*="language-"] { 35 | padding: 0.1em; 36 | border-radius: 0.3em; 37 | white-space: normal; 38 | } 39 | .token.cdata, 40 | .token.comment, 41 | .token.doctype, 42 | .token.prolog { 43 | color: #8292a2; 44 | } 45 | .token.punctuation { 46 | color: #f8f8f2; 47 | } 48 | .token.namespace { 49 | opacity: 0.7; 50 | } 51 | .token.constant, 52 | .token.deleted, 53 | .token.property, 54 | .token.symbol, 55 | .token.tag { 56 | color: #f92672; 57 | } 58 | .token.boolean, 59 | .token.number { 60 | color: #ae81ff; 61 | } 62 | .token.attr-name, 63 | .token.builtin, 64 | .token.char, 65 | .token.inserted, 66 | .token.selector, 67 | .token.string { 68 | color: #a6e22e; 69 | } 70 | .language-css .token.string, 71 | .style .token.string, 72 | .token.entity, 73 | .token.operator, 74 | .token.url, 75 | .token.variable { 76 | color: #f8f8f2; 77 | } 78 | .token.atrule, 79 | .token.attr-value, 80 | .token.class-name, 81 | .token.function { 82 | color: #e6db74; 83 | } 84 | .token.keyword { 85 | color: #66d9ef; 86 | } 87 | .token.important, 88 | .token.regex { 89 | color: #fd971f; 90 | } 91 | .token.bold, 92 | .token.important { 93 | font-weight: 700; 94 | } 95 | .token.italic { 96 | font-style: italic; 97 | } 98 | .token.entity { 99 | cursor: help; 100 | } 101 | -------------------------------------------------------------------------------- /_posts/catching-bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Поиск ошибок 3 | order: 6 4 | 5 | links: 6 | - text: 'Локализуйте проблему' 7 | url: '#catching-bugs' 8 | - text: 'Используйте цветные обводки' 9 | url: '#outlines' 10 | --- 11 | 12 |
13 |

Умение находить ошибки в коде не менее важно, чем умение писать код. И ошибка может оказаться, например, не вашей ошибкой, а особенностью браузера, но в любом случае, прежде чем что-то чинить, нужно как следует разобраться в проблеме.

14 | 15 |

Находить ошибки поможет пара простых приёмов.

16 |
17 | 18 |
    19 |
  1. 20 |

    Локализуйте проблему

    21 | 22 |

    Бывает, что на странице что-то пошло не так, но совершенно непонятно что вызывает эту проблему. Например, появляется горизонтальная прокрутка или одна из колонок падает вниз.

    23 | 24 |

    Дело в каком-то из элементов на странице, но как понять в каком? Для поиска виновника просто удаляйте все элементы по одному, пока не обнаружите нужный. Это удобнее всего делать в веб-инспекторе браузера во вкладке Elements. Выбранный элемент можно удалить нажав Backspace или Delete, а вернуть обратно — нажав Ctrl + Z.

    25 | 26 |

    Ошибки в CSS ищутся аналогично: в том же веб-инспекторе можно включать и выключать правила, пробовать разные комбинации, пока не обнаружится то, что вызывает проблему.

    27 | 28 |

    Также можно делать и в файлах с разметкой и стилями, пряча в комментарии отдельные строки или целые куски кода, можно спрятать хоть все элементы на странице. Когда виновник будет найден, просто вернёте всё на место.

    29 | 30 |

    Главная цель этих действий — найти проблемный элемент, чтобы понять какую часть соответствующего кода вам нужно рассмотреть повнимательней. Не нужно прочесывать весь код — просто скройте то, что точно работает, и сосредоточьтесь на той части, где спряталась ошибка.

    31 |
  2. 32 | 33 |
  3. 34 |

    Используйте цветные обводки

    35 | 36 |

    Чтобы видеть границы элементов и лучше понимать как они взаимодействуют друг с другом, добавьте им цветные обводки (outline). Результат может выглядеть как-то так:

    37 | 38 | 43 | 44 |

    Это позволит лучше представлять себе структуру документа, а ещё так удобно искать причины появления лишних прокруток на странице: может оказаться, что какие-то элементы получились шире, чем вы ожидали — с помощью разноцветных обводок вы легко сможете их обнаружить.

    45 | 46 |

    Аутлайны можно добавлять как в веб-инспекторе браузера, так и в файлы со стилями. Если на странице получилась разноцветная мешанина, обводки, которые временно не нужны, можно закомментировать или просто удалить.

    47 | 48 |

    Аутлайны можно оставлять на всё время разработки, главное перед публикацией финального результата не забыть убрать из кода всё лишнее.

    49 | 50 |

    Примеры кода и способы использования

    51 |
  4. 52 |
53 | -------------------------------------------------------------------------------- /_posts/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Введение 3 | order: 0 4 | 5 | links: 6 | - text: 'Используйте HTML-тэги по смыслу' 7 | url: '#sense' 8 | - text: 'Используйте БЭМ для именования классов' 9 | url: '#bem' 10 | - text: 'Используйте в названиях классов простые и короткие слова' 11 | url: '#common-words' 12 | --- 13 | 14 |
15 |

Разметка страницы может быть непростым занятием, особенно поначалу. Какой тег выбрать? Какой класс добавить? Какой должна быть разметка, чтобы стили одного элемента не поломали другой?

16 | 17 |

На все эти случаи есть простые правила, следуя которым вы сможете писать легко писать чистый, хорошо структурированный HTML-код, который будет удобно читать и приятно поддерживать.

18 |
19 | 20 |
    21 |
  1. 22 |

    Используйте HTML-тэги по смыслу

    23 | 24 |
    25 |

    Элементы для основной раскладки

    26 | 27 |
      28 |
    • header — шапка страницы или блока.
    • 29 |
    • footer — подвал страницы или блока.
    • 30 |
    • main — главная смысловая часть страницы.
    • 31 |
    • section — разделы внутри основного контента.
    • 32 |
    • article — отдельная статья, пост или комментарий.
    • 33 |
    • nav — навигация, ссылки для перемещения по сайту.
    • 34 |
    • aside — боковая колонка, дополнительный контент не входящий в main.
    • 35 |
    36 | 37 |

    Элементы для содержимого

    38 | 39 |
      40 |
    • h1-h6 — заголовки. Обычно h1 — это название сайта. Заголовки нужно использовать в порядке иерархии, это важно для доступности.
    • 41 |
    • ul и ol — списки, в них удобно размещать любые перечисляемые элементы.
    • 42 |
    • button — кнопка, например, элемент управления или кнопка для отправки формы.
    • 43 |
    44 | 45 |

    Для элементов без особой смысловой нагрузки можно использовать div или span.

    46 | 47 |

    Для разметки страницы нельзя использовать теги, предназначенные для оформления текста: например, b и i. У них есть собственные стили, которые со временем может понадобиться переопределить или сбросить — проще сразу выбрать элемент, у которого нет стилей по умолчанию. Тег p уместно использовать для блоков текста, но для других случаев лучше выбрать div.

    48 | 49 |

    Это далеко не все теги, которые существуют: вот здесь есть удобный список тегов, сгруппированных по смыслу, с комментариями и примерами кода.

    50 | 51 |

    Не знаете с какой стороны взяться за дело? Начните со статьи Первые шаги.

    52 |
    53 |
  2. 54 | 55 |
  3. 56 |

    Используйте БЭМ для именования классов

    57 | 58 |
    59 |

    Подробно и с примерами читайте в статье Как писать классы по БЭМ?

    60 |
    61 |
  4. 62 | 63 |
  5. 64 |

    Используйте в названиях классов простые и короткие слова

    65 | 66 |

    Вот здесь приведены примеры таких слов для разных случаев.

    67 |
  6. 68 |
69 | -------------------------------------------------------------------------------- /.github/workflows/nextjs.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Next.js site to GitHub Pages 2 | # 3 | # To get started with Next.js see: https://nextjs.org/docs/getting-started 4 | # 5 | name: Deploy Next.js site to Pages 6 | 7 | on: 8 | # Runs on pushes targeting the default branch 9 | push: 10 | branches: ["master"] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 22 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 23 | concurrency: 24 | group: "pages" 25 | cancel-in-progress: false 26 | 27 | jobs: 28 | # Build job 29 | build: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Detect package manager 35 | id: detect-package-manager 36 | run: | 37 | if [ -f "${{ github.workspace }}/yarn.lock" ]; then 38 | echo "manager=yarn" >> $GITHUB_OUTPUT 39 | echo "command=install" >> $GITHUB_OUTPUT 40 | echo "runner=yarn" >> $GITHUB_OUTPUT 41 | exit 0 42 | elif [ -f "${{ github.workspace }}/package.json" ]; then 43 | echo "manager=npm" >> $GITHUB_OUTPUT 44 | echo "command=ci" >> $GITHUB_OUTPUT 45 | echo "runner=npx --no-install" >> $GITHUB_OUTPUT 46 | exit 0 47 | else 48 | echo "Unable to determine package manager" 49 | exit 1 50 | fi 51 | - name: Setup Node 52 | uses: actions/setup-node@v4 53 | with: 54 | node-version: "20" 55 | cache: ${{ steps.detect-package-manager.outputs.manager }} 56 | - name: Setup Pages 57 | uses: actions/configure-pages@v4 58 | with: 59 | # Automatically inject basePath in your Next.js configuration file and disable 60 | # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized). 61 | # 62 | # You may remove this line if you want to manage the configuration yourself. 63 | static_site_generator: next 64 | - name: Restore cache 65 | uses: actions/cache@v3 66 | with: 67 | path: | 68 | .next/cache 69 | # Generate a new cache whenever packages or source files change. 70 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} 71 | # If source files changed but packages didn't, rebuild from a prior cache. 72 | restore-keys: | 73 | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}- 74 | - name: Install dependencies 75 | run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} 76 | - name: Build with Next.js 77 | run: ${{ steps.detect-package-manager.outputs.runner }} next build 78 | - name: Static HTML export with Next.js 79 | run: ${{ steps.detect-package-manager.outputs.runner }} next export 80 | - name: Upload artifact 81 | uses: actions/upload-pages-artifact@v2 82 | with: 83 | path: ./out 84 | 85 | # Deployment job 86 | deploy: 87 | environment: 88 | name: master 89 | url: ${{ steps.deployment.outputs.page_url }} 90 | runs-on: ubuntu-latest 91 | needs: build 92 | steps: 93 | - name: Deploy to GitHub Pages 94 | id: deployment 95 | uses: actions/deploy-pages@v3 96 | -------------------------------------------------------------------------------- /src/styles/global.scss: -------------------------------------------------------------------------------- 1 | @import "./themes.scss"; 2 | 3 | HTML { 4 | font-size: 12px; 5 | } 6 | 7 | BODY { 8 | margin: 0; 9 | padding: 0; 10 | padding-top: 1rem; 11 | background-color: var(--color-background); 12 | font: 16px/1.5 Trebuchet MS, sans-serif; 13 | color: var(--color-text); 14 | } 15 | 16 | BODY:not(.keyboard-navigation) * { 17 | outline: none; 18 | } 19 | 20 | * { 21 | scroll-behavior: smooth; 22 | } 23 | 24 | /* Links 25 | --------------------------------------------- */ 26 | 27 | A { 28 | color: var(--color-link); 29 | text-decoration: underline; 30 | transition: all 0.25s; 31 | 32 | &:visited { 33 | color: var(--color-link-visited); 34 | } 35 | 36 | &:hover { 37 | color: var(--color-link-hover); 38 | text-decoration: none; 39 | } 40 | } 41 | 42 | /* Headers 43 | --------------------------------------------- */ 44 | 45 | H1, 46 | H2, 47 | H3, 48 | H4, 49 | H5 { 50 | margin: 0.5em 0; 51 | } 52 | 53 | H2 { 54 | font-size: 1.8rem; 55 | 56 | @media (min-width: 800px) { 57 | font-size: 2.5rem; 58 | } 59 | } 60 | 61 | H3 { 62 | font-size: 1.5rem; 63 | 64 | @media (min-width: 800px) { 65 | font-size: 2rem; 66 | } 67 | 68 | SUP { 69 | font-size: 0.7rem; 70 | opacity: 0.5; 71 | 72 | &:hover { 73 | opacity: 1; 74 | } 75 | } 76 | } 77 | 78 | H4 { 79 | font-size: 1.2rem; 80 | 81 | @media (min-width: 800px) { 82 | font-size: 1.5rem; 83 | } 84 | } 85 | 86 | H5 { 87 | font-size: 1.25rem; 88 | } 89 | 90 | /* Images 91 | --------------------------------------------- */ 92 | 93 | FIGURE { 94 | margin: 0; 95 | } 96 | 97 | IMG { 98 | border: 1px solid var(--color-image-border); 99 | max-width: 100%; 100 | height: auto; 101 | } 102 | 103 | /* Lists 104 | --------------------------------------------- */ 105 | 106 | OL, 107 | UL { 108 | padding-left: 0; 109 | 110 | @media (min-width: 600px) { 111 | padding-left: 20px; 112 | } 113 | @media (min-width: 800px) { 114 | padding-left: 30px; 115 | } 116 | } 117 | 118 | LI { 119 | margin-bottom: 0.5rem; 120 | } 121 | 122 | DL { 123 | margin-bottom: 2rem; 124 | } 125 | 126 | DT { 127 | font-weight: bold; 128 | } 129 | 130 | LI > DL > DT { 131 | font-size: 1.1em; 132 | } 133 | 134 | DD > DL { 135 | margin-top: 0.5rem; 136 | } 137 | 138 | DD:not(:last-child) { 139 | margin-bottom: 0.5rem; 140 | } 141 | 142 | /* Counters 143 | --------------------------------------------- */ 144 | 145 | OL { 146 | counter-reset: list-counter; 147 | list-style: none; 148 | } 149 | 150 | OL OL { 151 | list-style-type: decimal; 152 | padding-left: 2.4em; 153 | 154 | @media (min-width: 600px) { 155 | padding-left: 3em; 156 | } 157 | } 158 | 159 | OL > LI { 160 | counter-increment: list-counter; 161 | } 162 | OL > LI > H2:before { 163 | content: counter(list-counter) ". "; 164 | } 165 | 166 | .list--has-numbers { 167 | list-style-type: decimal; 168 | } 169 | 170 | /* Codes 171 | --------------------------------------------- */ 172 | 173 | PRE, 174 | CODE { 175 | border: 1px solid var(--color-code-border); 176 | border-radius: 3px; 177 | background-color: var(--color-code-background); 178 | font-size: 15px; 179 | } 180 | 181 | CODE { 182 | padding: 1px 5px; 183 | } 184 | 185 | PRE { 186 | padding: 8px 12px; 187 | overflow-x: auto; 188 | 189 | > code { 190 | border: 0; 191 | padding-right: 0; 192 | padding-left: 0; 193 | } 194 | } 195 | 196 | .language-css, 197 | .language-html { 198 | font-size: 13px; 199 | } 200 | 201 | /* Tip 202 | --------------------------------------------- */ 203 | 204 | .tip { 205 | font-style: italic; 206 | } 207 | 208 | /* Attention 209 | --------------------------------------------- */ 210 | 211 | .attention { 212 | padding-left: 1.8rem; 213 | border-left: 5px solid darkorange; 214 | } 215 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "at-rule-empty-line-before": [ 4 | "always", 5 | { 6 | "except": ["blockless-group", "first-nested"], 7 | "message": "Ожидается пустая строка перед @-правилом" 8 | } 9 | ], 10 | "at-rule-semicolon-newline-after": [ 11 | "always", 12 | { 13 | "message": "Каждое @-правило должно быть на новой строке" 14 | } 15 | ], 16 | "block-no-empty": [ 17 | true, 18 | { 19 | "message": "Пустых блоков быть не должно" 20 | } 21 | ], 22 | "declaration-colon-space-after": [ 23 | "always", 24 | { 25 | "message": "Ожидается пробел после двоеточия" 26 | } 27 | ], 28 | "declaration-colon-space-before": [ 29 | "never", 30 | { 31 | "message": "Перед двоеточием не нужен пробел" 32 | } 33 | ], 34 | "declaration-block-no-duplicate-properties": [ 35 | true, 36 | { 37 | "message": "Дублирующиеся правила" 38 | } 39 | ], 40 | "declaration-block-no-ignored-properties": [ 41 | true, 42 | { 43 | "message": "Это правило не будет работать из-за других правил, заданных для этого элемента" 44 | } 45 | ], 46 | "declaration-block-no-shorthand-property-overrides": [ 47 | true, 48 | { 49 | "message": "Сокращенная запись перезапишет стили, заданные выше" 50 | } 51 | ], 52 | "declaration-block-semicolon-newline-after": [ 53 | "always", 54 | { 55 | "message": "Нужна новая строка после точки с запятой" 56 | } 57 | ], 58 | "declaration-block-single-line-max-declarations": [ 59 | 1, 60 | { 61 | "message": "На строке должно быть только одно правило" 62 | } 63 | ], 64 | "declaration-block-trailing-semicolon": [ 65 | "always", 66 | { 67 | "message": "Каждое правило следует заканчивать точкой с запятой" 68 | } 69 | ], 70 | "block-closing-brace-newline-before": [ 71 | "always", 72 | { 73 | "message": "Закрывающая скобка должна быть на новой строке" 74 | } 75 | ], 76 | "block-no-single-line": [ 77 | true, 78 | { 79 | "message": "Не следует писать блок в одну строку" 80 | } 81 | ], 82 | "block-opening-brace-space-before": [ 83 | "always", 84 | { 85 | "message": "Нужен пробел перед открывающей фигурной скобкой" 86 | } 87 | ], 88 | "block-opening-brace-newline-after": [ 89 | "always", 90 | { 91 | "message": "Нужен перенос после открывающей фигурной скобки" 92 | } 93 | ], 94 | "declaration-no-important": [ 95 | true, 96 | { 97 | "message": "Не следует использовать !important" 98 | } 99 | ], 100 | "indentation": [ 101 | 2, 102 | { 103 | "message": "Отступы должны быть кратны 2-м пробелам" 104 | } 105 | ], 106 | "length-zero-no-unit": [ 107 | true, 108 | { 109 | "message": "Нулевым значениям можно не указывать единицы измерения" 110 | } 111 | ], 112 | "media-feature-colon-space-after": [ 113 | "always", 114 | { 115 | "message": "Ожидается пробел после двоеточия в медиавыражении" 116 | } 117 | ], 118 | "no-duplicate-selectors": [ 119 | true, 120 | { 121 | "message": "Не следует дублировать селекторы" 122 | } 123 | ], 124 | "number-leading-zero": [ 125 | "always", 126 | { 127 | "message": "В числовом значении перед точкой ожидается ноль" 128 | } 129 | ], 130 | "number-max-precision": [ 131 | 3, 132 | { 133 | "message": "В значениях достаточно 3-х знаков после запятой" 134 | } 135 | ], 136 | "rule-nested-empty-line-before": [ 137 | "always", 138 | { 139 | "except": ["first-nested"], 140 | "message": "Перед вложенным правилом ожидается пустая строка" 141 | } 142 | ], 143 | "rule-non-nested-empty-line-before": [ 144 | "always", 145 | { 146 | "message": "Ожидается пустая строка перед правилом" 147 | } 148 | ], 149 | "selector-pseudo-element-colon-notation": [ 150 | "double", 151 | { 152 | "message": "Для псевдоэлементов следует использовать двойное двоеточие" 153 | } 154 | ], 155 | "selector-no-id": [ 156 | true, 157 | { 158 | "message": "Для стилизации не следует использовать ID" 159 | } 160 | ], 161 | "selector-list-comma-newline-after": [ 162 | "always", 163 | { 164 | "message": "Каждый селектор должен быть на новой строке" 165 | } 166 | ], 167 | "string-quotes": [ 168 | "double", 169 | { 170 | "message": "Ожидаются двойные кавычки" 171 | } 172 | ], 173 | "selector-type-no-unknown": [ 174 | true, 175 | { 176 | "message": "Такого элемента не существует" 177 | } 178 | ], 179 | "value-no-vendor-prefix": [ 180 | true, 181 | { 182 | "message": "Не нужно использовать вендорные префиксы" 183 | } 184 | ], 185 | "unit-no-unknown": [ 186 | true, 187 | { 188 | "message": "Некорректные единицы измерения" 189 | } 190 | ] 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /_posts/drop-of-magic.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Капля магии 3 | order: 4 4 | 5 | links: 6 | - text: 'Цель вижу, в себя верю!' 7 | url: '#target' 8 | - text: 'Всем выйти из сумрака!' 9 | url: '#dusk' 10 | 11 | additional_links: 12 | - text: 'Центрирование в CSS: полное руководство' 13 | url: https://css-tricks.com/centering-css-complete-guide/ 14 | - text: 'Краш-тест вёрстки' 15 | url: https://isqua.ru/blog/2016/06/19/crash-test-viorstki/ 16 | - text: 'Контент по центру, фон по ширине' 17 | url: https://isqua.ru/blog/2016/06/23/content-po-sentru-fon-po-shirinie/ 18 | 19 | --- 20 | 21 |
22 |

Когда разметка будет готова, к ней потребуется оформление. Если добавить в ваши коды немного вспомогательных стилей, это поможет вам писать CSS проще и быстрее.

23 |
24 | 25 |
    26 |
  1. 27 |

    Цель вижу, в себя верю!

    28 | 29 |

    Чтобы аккуратно сверстать макет, ничего не потеряв по дороге, нужно или обладать хорошей зрительной памятью, или постоянно сверяться с макетом. Второе отнимает массу времени, и всё равно можно что-то упустить. Для решения проблемы можно просто подложить макет под страницу:

    30 | 31 | ```html 32 | 37 | ``` 38 | 39 |

    Код вставляется в элемент <head>.

    40 | 41 |

    Если у вас есть макеты для разных разрешений экрана, их можно менять с помощью медиавыражений:

    42 | 43 | ```html 44 | 61 | ``` 62 | 63 |

    Чтобы фоны и картинки на странице не загораживали макеты, можно добавить opacity для <body>:

    64 | 65 | ```html 66 | 87 | ``` 88 | 89 |

    Чтобы удобно управлять прозрачностью, можно установить Pixel Glass:

    90 | 91 | 92 |
      93 |
    1. Установите скрипт в папку с проектом: 94 | 95 | ```js 96 | npm i pixel-glass --save-dev 97 | ``` 98 |
    2. 99 | 100 |
    3. Добавьте в <head> подключение файлов скрипта: 101 | 102 | ```html 103 | 104 | 105 | ``` 106 | 107 |
    4. 108 |
    109 | 110 |

    После этого на странице появится вот такая панель: 111 |

    112 | Панель Pixel Glass 113 | 114 |

    Она позволит управлять прозрачностью <body> или выключить её совсем, если она не нужна.

    115 | 116 |
    117 |

    Внимание: если локальный сервер смотрит не в корень проекта, а в другую директорию (например, source), при запуске сервера npm-пакеты окажутся снаружи этой директории, и будут недоступны. В этом случае pixel-glass нужно устанавливать в директорию, куда смотрит сервер, склонировав туда пакет с гитхаба:

    118 | 119 | 120 | ```js 121 | git clone git@github.com:yoksel/pixel-glass-js.git pixel-glass 122 | ``` 123 | 124 |

    И подключать скрипты и стили оттуда:

    125 | 126 | ```html 127 | 128 | 129 | ``` 130 |
    131 | 132 |

    Таким образом макеты будут всё время перед глазами и вам не придётся тратить время на переключение в графический редактор и обратно. Особенно это удобно при работе на маленьком экране, когда не получается разместить рядом фотошоп и браузер.

    133 | 134 |

    При использовании этого способа в какой-то момент текст страницы наложится на текст макета, и получится нечитаемая каша. Чтобы отличить их друг от друга, можно CSS-ом раскрасить текст в яркие цвета (например, color: crimson). Получится примерно вот такое:

    135 | 136 | Цветной текст 137 | 138 |

    Сразу видно где страница не совпадает с макетом и что нужно подправить.

    139 |
  2. 140 |
  3. 141 |

    Всем выйти из сумрака!

    142 | 143 |

    Раскладывая элементы по странице, очень удобно видеть где они начинаются и где заканчиваются. Самый простой способ добавить границы выглядит так:

    144 | 145 | ```css 146 | .yourclass { 147 | outline: 2px solid deeppink; 148 | } 149 | ``` 150 | 151 |

    Почему outline, а не border? outline не влияет на блочную модель элемента и не меняет его размеры, как это делает border.

    152 | 153 |

    Чтобы добавить обводки сразу нескольким крупным блокам, можно использовать такой код:

    154 | 155 | ```css 156 | BODY > * { 157 | outline: 2px solid deeppink; 158 | } 159 | BODY > * > * { 160 | outline: 2px dashed lime; 161 | outline-offset: -2px; 162 | } 163 | BODY > * > * > * { 164 | outline: 2px dotted dodgerblue; 165 | outline-offset: -4px; 166 | } 167 | ``` 168 | 169 |

    В действии это выглядит примерно так:

    170 | 171 | 176 | 177 |

    Этот код тоже можно вставить в head или лучше положить его в файл с стилями, но можно сделать ещё удобнее: добавьте в браузер Stylish (плагин для кастомного CSS) и подключите этот код через него — тогда в любой момент вы сможете включить или выключить обводки не покидая страницу.

    178 | 179 |

    Также можно добавить ещё больше обводок или задать свои цвета (цвета удобно задавать названиями).

    180 |
  4. 181 |
182 | -------------------------------------------------------------------------------- /_posts/first-steps.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Первые шаги 3 | order: 1 4 | 5 | links: 6 | - text: 'Cтруктура документа' 7 | url: '#structure' 8 | - text: 'Как правильно выбрать тег?' 9 | url: '#tags' 10 | - text: 'Как выбрать класс?' 11 | url: '#class-names' 12 | - text: 'Чистый код' 13 | url: '#clear-code' 14 | 15 | additional_links: 16 | - url: 'https://youtu.be/atXxkKjPbN8' 17 | text: 'Зачем нужны заголовки — HTML Шорты' 18 | --- 19 | 20 |
21 | Предположим, вам нужно сверстать макет. На первый взгляд он может быть похож на большую каракатицу, к которой 22 | непонятно как подступиться, но если разложить всё по полочкам, справиться будет гораздо проще. 23 |
24 | 25 |
    26 |
  1. 27 |

    Cтруктура документа

    28 | 29 |

    Смотрите на компоненты страницы как на кубики, из которых вам надо собрать единое целое. Двигайтесь от простого к 30 | сложному, от крупных элементов к мелким. Слона удобнее есть по частям.

    31 | 32 |

    Прежде всего надо разметить основные блоки страницы: шапку, подвал и основное содержимое. Чтобы разметка не 33 | превращалась в кашу из дивов, используйте подходящие HTML-элементы:

    34 | 35 |
      36 |
    • header — шапка (страницы или блока).
    • 37 |
    • footer — подвал (страницы или блока).
    • 38 |
    • main — главное содержимое страницы.
    • 39 |
    • aside — боковая колонка, дополнительный контент не входящий в main.
    • 40 |
    41 | 42 |

    Получается такая структура:

    43 | 44 | ```html 45 |
    46 |
    47 | 48 |
    49 | ``` 50 | 51 |

    Элемента aside может не быть, остальное, как правило, есть.

    52 | 53 |

    Здесь в классах удобно использовать названия элементов, а чтобы было понятно, что это основные блоки страницы, 54 | можно использовать префиксы page-... или site-.... Кроме того, так классы не будут 55 | пересекаться c шапками и подвалами вложенных элементов:

    56 | 57 | ```html 58 | 59 |
    60 | 61 |
    62 | ``` 63 | 64 |

    Теперь эти крупные контейнеры нужно заполнить более мелкими внутренними элементами.

    65 |
  2. 66 | 67 |
  3. 68 |

    Как правильно выбрать тег?

    69 | 70 |

    Это несложно, если знать простые правила:

    71 | 72 |
    73 |
    Это раздел внутри main?
    74 |
    75 | Скорее всего, вам нужен тег section. 76 |
    77 | 78 |
    Это пост в блоге, статья или комментарий?
    79 |
    Используйте article.
    80 | 81 |
    Это заголовок?
    82 |
    83 | Используйте теги заголовков h1-h6 соответствующего уровня. Обычно h1 — это название 84 | сайта, и заголовок такого уровня должен быть на странице один. Все последующие заголовки должны задаваться в 85 | иерархической последовательности. Например, заголовок текущей страницы — h2, заголовок раздела на 86 | странице — h3, а заголовок подраздела уровнем ниже — h4. 87 |
    88 | 89 |
    Это навигация или меню?
    90 |
    Используйте тег nav. Внутри могут находиться просто ссылки либо ссылки внутри списка — зависит от 91 | вашего макета.
    92 | 93 |
    Это несколько однотипных элементов?
    94 |
    Скорее всего, вам нужно сгруппировать их с помощью ul или ol.
    95 | 96 |
    Этот элемент можно кликнуть?
    97 |
    98 |
    99 |
    По клику происходит действие на странице либо отправка формы?
    100 |
    Используйте button.
    101 | 102 |
    По клику происходит переход на другую страницу?
    103 |
    Это простая ссылка, a.
    104 | 105 |
    106 |
    107 | 108 |
    Это картинка?
    109 | 110 |
    111 |
    112 |
    Это иллюстрация в статье, товар в каталоге магазина или картинка в фотогалерее?
    113 |
    Это изображение относится к содержимому страницы, вам нужен img, обязательно укажите текст в 114 | alt="".
    115 | 116 |
    Это иконка?
    117 |
    Иконку можно вставить псевдоэлементами либо инлайновым svg. Псевдоэлементы помогут сохранить 118 | более чистый код. Если иконка задаётся кнопке, до применения стилей в кнопке должен быть текст.
    119 | 120 |
    Если эту картинку убрать, смысл блока не потеряется?
    121 |
    Это изображение лучше добавить с помощью CSS.
    122 |
    123 |
    124 | 125 |
    Это ярлык для текстового поля?
    126 |
    Используйте label. Чтобы связать лейбл с полем, поместите инпут в лейбл либо свяжите их атрибутом 127 | for="".
    128 | 129 |
    Это абзац в тексте?
    130 |
    Используйте p.
    131 | 132 |
    Ничего не подошло?
    133 |
    Смело используйте div или span.
    134 | 135 |
    136 | 137 |

    Если вы на этом шаге всё сделали правильно, открыв вашу страницу в браузере, вы увидите хорошо структурированный 138 | документ с чётко прослеживаемой иерархией, причём ещё до подключения стилей к странице.

    139 | 140 |

    Посмотреть структуру страницы в виде дерева можно здесь: yoksel.github.io/html-tree/.

    142 |
  4. 143 | 144 |
  5. 145 |

    Как выбрать класс?

    146 | 147 |

    Для многих элементов страницы уже есть устоявшиеся названия классов, и вы можете просто выбрать подходящий класс 148 | в этом справочнике.

    149 | 150 |

    Также вам может пригодиться статья Как писать классы по БЭМ?

    151 |
  6. 152 | 153 |
  7. 154 |

    Чистый код

    155 | 156 |

    Очень важно поддерживать код в порядке: это поможет, в первую очередь, вам самим не заблудиться в коде, когда он 157 | станет слишком большим. Для этого:

    158 | 159 |
    160 |
    Разделяйте блоки переносами
    161 |
    Чтобы было видно где закончился один блок и начался другой.
    162 | 163 |
    Отступы в коде должны отражать вложенность элементов друг в друга
    164 |
    Чтобы лучше видеть структуру документа.
    165 | 166 |
    Сохраняйте единообразие в коде
    167 |
    Все схожие элементы должны быть размечены в едином стиле и использовать однотипные классы. Например, нельзя 168 | использовать для картинок одновременно .picture и .img — выберите что-то одно.
    169 |
    170 |
  8. 171 |
172 | -------------------------------------------------------------------------------- /_posts/check-code.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Тестируем всё 3 | order: 5 4 | 5 | links: 6 | - text: 'Проверить HTML' 7 | url: '#check-html' 8 | - text: 'Проверить БЭМ-разметку' 9 | url: '#check-bem' 10 | - text: 'Проверить CSS' 11 | url: '#check-css' 12 | - text: 'Проверить страницу на разных размерах экрана' 13 | url: '#devices' 14 | - text: 'Проверить страницу на соответствие макету' 15 | url: '#check-maket' 16 | - text: 'Проверить шрифты' 17 | url: '#check-fonts' 18 | - text: 'Проверить изображения' 19 | url: '#check-images' 20 | - text: 'Проверить на переполнение контентом' 21 | url: '#check-overflow' 22 | 23 | --- 24 | 25 |
26 |

Как проверить, что вы всё сделали правильно? Что в разметке и в стилях нет ошибок и что результат соответствует макету? Можно воспользоваться разными инструментами, каждый для своих целей.

27 |
28 | 29 |
    30 |
  1. 31 |

    Проверить HTML

    32 | 33 |

    Используйте валидатор разметки.

    34 | 35 |

    Он проверит ваш HTML на соответствие спецификациям, а так же поможет найти простые ошибки, вроде незакрытых тегов.

    36 | 37 |

    Подсказка: если валидатор говорит, что в section и article обязательно должны быть заголовки, добавьте их в разметку. Если в макете нет текста, подходящего по смыслу, можно сделать скрытые заголовки используя способ с классом .visuallyhidden, это важно для доступности.

    38 |
  2. 39 | 40 |
  3. 41 |

    Проверить БЭМ-разметку

    42 | 43 |

    Используйте Html tree.

    44 | 45 |

    Инструмент построит структуру страницы, проверит БЭМ-разметку на самые простые ошибки и покажет иерархию заголовков.

    46 |
  4. 47 | 48 |
  5. 49 |

    Проверить CSS

    50 | 51 |

    Валидация

    52 | 53 |

    Используйте CSS syntax validator.

    54 | 55 |

    Форматирование

    56 | 57 |

    Используйте The stylelint CLI.

    58 | 59 |

    Установка:

    60 | 61 | ```js 62 | npm install -g stylelint 63 | ``` 64 | 65 |

    Также нужен файл с правилами проверки стилей. Можно взять мой набор правил, или накликать свой используя Stylelint Config Generator.

    66 | 67 |

    Список правил сохраните в корне проекта в файл с названием .stylelintrc (расширение не нужно).

    68 | 69 |

    Файл с правилами можно редактировать под ваши нужды, узнать больше о правилах можно здесь.

    70 | 71 |

    Использование:

    72 | 73 | SASS: 74 | ```js 75 | stylelint "sass/**/*.scss" 76 | ``` 77 | 78 | LESS: 79 | ```js 80 | stylelint "less/**/*.less" 81 | ``` 82 | 83 |

    Команда запускается в папке проекта.

    84 | 85 |
  6. 86 | 87 |
  7. 88 |

    Проверить страницу на разных размерах экрана

    89 | 90 |

    Используйте эмулятор мобильных устройств, который есть в Хроме. Кнопка включения находится в панели разработчиков, вторая иконка в верхнем ряду:

    91 | 92 | Кнопка включения эмулятора мобильных устройств 93 | 94 |

    В эмуляторе в выпадающем меню можно просто выбрать устройство с подходящими размерами, а можно кликнуть «Edit» и добавить свои:

    95 | 96 | Добавить устройство 97 | 98 |

    Справа на скриншоте есть выпадушка с выбором типа устройства (на скриншоте Mobile), эта опция влияет на наличие прокрутки на странице. Чтобы прокрутка не отъедала ширину страницы, между Mobile и Desktop всегда выбирайте Mobile.

    99 | 100 |

    Там же можно скрыть устройства, которые вам не нужны.

    101 | 102 |

    Мой список устройств выглядит так:

    103 | 104 | Список устройств 105 | 106 |

    Такой поход избавляет от необходимости подбирать размер окна руками, а так же позволяет быстро переключаться между вьюпортам, причём именно теми, которые нужны вам.

    107 | 108 |

    Не забывайте также проверять макеты на очень широких экранах (1400+), чтобы убедиться что и в этом случае страница не разваливается.

    109 |
  8. 110 | 111 |
  9. 112 |

    Проверить страницу на соответствие макету

    113 | 114 |

    Используйте Pixel Glass или расширение для Хрома PerfectPixel.

    115 | 116 |

    Pixel Glass больше подходит для работы над проектом с адаптивной вёрсткой (макеты будут меняться сами при изменении размеров окна), PerfectPixel — для быстрой проверки страниц.

    117 |
  10. 118 | 119 |
  11. 120 |

    Проверить шрифты

    121 | 122 |

    Панель разработчика → Network → Fonts.

    123 | 124 |

    Подсказка: проверьте, что в браузерах с поддержкой woff2 загрузится именно этот формат. Если грузится woff, проверьте порядок перечисления шрифтов. Браузер выбирает первый подходящий, а не оптимальный из перечисленных.

    125 |
  12. 126 | 127 |
  13. 128 |

    Проверить изображения

    129 | 130 |

    Панель разработчика → Network → Img.

    131 | 132 |

    Плотность пикселей устройства можно выбрать в меню эмулятора:

    133 | 134 | Меню выбора плотности пикселей 135 | 136 |

    Если там нет такой опции, нажмите на три точки в правой части панели устройств и включите её в выпадающем меню:

    137 | 138 | Включить меню выбора плотности пикселей 139 | 140 |

    Если все адаптивные изображения содержат в своём адресе @, этот спецсимвол можно использовать для фильтрации, чтобы в панели показывались только те картинки, которые нужно проверить:

    141 | 142 | Панель сетевых загрузок, картинки 143 | 144 |

    В панели устройств меняйте размеры экрана и плотность пикселей и смотрите какие изображения загрузились.

    145 |
  14. 146 | 147 |
  15. 148 |

    Проверить страницу на переполнение контентом

    149 | 150 |

    Введите в консоль браузера команду:

    151 | 152 | ```js 153 | document.body.contentEditable = true 154 | ``` 155 | 156 |

    После этого вы сможете отредактировать любой текстовый элемент на странице и добавить в него текст. Это позволит проверить поведение страницы в случаях когда контента слишком мало или наоборот — слишком много.

    157 | 158 |

    При добавлении текста элементы должны тянуться по вертикали, текст не должен упираться в края элемента или обрезаться.

    159 | 160 |

    Статья по теме: Краш-тест вёрстки.

    161 |
  16. 162 |
163 | -------------------------------------------------------------------------------- /_posts/bem-rules.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Как писать классы по БЭМ? 3 | order: 2 4 | 5 | links: 6 | - text: 'Простой пример: Блок + Элемент' 7 | url: '#block-elem' 8 | - text: 'Пример посложнее: Блок + Элемент + Модификатор' 9 | url: '#block-elem-mod' 10 | - text: 'Ещё сложнее: что делать, если хочется сделать элемент элемента?' 11 | url: '#elem-elem' 12 | --- 13 | 14 |
15 |

БЭМ расшифровывается как «Блок Элемент Модификатор». На самом деле, это целый стэк технологий, из которого мы воспользуемся только соглашением по именованию классов.

16 | 17 |

Почему БЭМ?

18 | 19 |
    20 |
  • БЭМ позволяет создавать абсолютно независимые блоки. Блоки и элементы получают уникальные имена, так что стили для одного элемента ничего не поломают в другом.
  • 21 |
  • БЭМ помогает легко придумывать любое количество классов, не повторяющихся между собой.
  • 22 |
  • БЭМ помогает писать самодокументирующийся код, в классе любого элемента содержится информация о нём.
  • 23 |
24 | 25 |

Подробнее можно почитать в разделах Быстрый старт и 26 | Часто задаваемые вопросы на сайте bem.info.

27 | 28 |

Ниже показаны примеры кода.

29 |
30 | 31 |
    32 |
  1. 33 |

    Простой пример: Блок + Элемент

    34 | 35 |

    Допустим, у вас есть блок с заголовком, текстом и кнопкой внутри, например, это всплывающее окно — попап. Разметка:

    36 | 37 | ```html 38 |
    39 |

    Заголовок

    40 |
    Текст
    41 | 42 |
    43 | ``` 44 | 45 |

    Добавляем класс содержащий назначение элемента: .popup:

    46 | 47 | ```html 48 | 53 | ``` 54 | 55 |

    Теперь попробуем добавить классы вложенным элементам:

    56 | 57 | ```html 58 | 63 | ``` 64 | 65 |

    Классы удобные, но не уникальные. Если на странице будут ещё элементы с классами .title и .text, их стили могут затронуть элементы в попапе. Селектор типа .popup .title может в будущем создать проблемы со специфичностью. Можно придумать другие классы, но чем больше похожих по смыслу элементов, тем сложнее придумывать новые классы.

    66 | 67 |

    А теперь применим БЭМ-нотацию: каждому элементу внутри блока добавим префикс с классом родителя, например, для заголовка это будет popup__title:

    68 | 69 | ```html 70 | 75 | ``` 76 | 77 |

    Теперь эти классы легко решают сразу две задачи: во-первых, благодаря уникальным классам стили для них никогда не пересекутся с другими подобными элементами на странице, а во-вторых, по таким классам сразу видно, что это элементы блока .popup.

    78 |
  2. 79 | 80 |
  3. 81 |

    Пример посложнее: Блок + Элемент + Модификатор

    82 | 83 |

    Для примера возьмём сервисное сообщение на сайте. Обычно такие сообщения бывают разных видов, например, сообщение об успешном завершении действия или об ошибке.

    84 | 85 | ```html 86 |
    87 |

    Заголовок сообщения

    88 |
    Текст сообщения
    89 |
    90 | ``` 91 | 92 |

    Логично использовать одну и ту же разметку, но с разными цветовыми темами. Именно здесь очень пригодятся модификаторы.

    93 | 94 | ```html 95 |
    96 |

    Заголовок сообщения

    97 |
    Текст сообщения
    98 |
    99 | 100 |
    101 |

    Заголовок сообщения

    102 |
    Текст сообщения
    103 |
    104 | ``` 105 | 106 |

    Обоим элементам можно добавить одинаковые стили используя общий класс .message и так же легко можно добавить отдельные стили для каждого из них, используя уникальный класс с модификатором:

    107 | 108 | ```css 109 | .message { 110 | border: 1px solid gray; 111 | } 112 | 113 | .message--success { 114 | border-color: green; 115 | } 116 | 117 | .message--error { 118 | border-color: red; 119 | } 120 | ``` 121 | 122 |

    Оба сообщения будут иметь рамку толщиной один пиксель, но для сообщения об успешной операции она будет зелёной, а для сообщения об ошибке — красной.

    123 |
  4. 124 | 125 |
  5. 126 |

    Ещё сложнее: что делать, если хочется сделать элемент элемента?

    127 | 128 |

    Например, на странице есть блок новостей:

    129 | 130 | ```html 131 |
    132 |

    Новости

    133 | 134 |
      135 |
    • 136 |
    • 137 |
    138 |
    139 | ``` 140 | 141 |

    Заголовок блока логично получает класс .news__title, список — .news__list, а отдельная новость — .news__item:

    142 | 143 | ```html 144 |
    145 |

    Новости

    146 | 147 |
      148 |
    • 149 |
    • 150 |
    151 |
    152 | ``` 153 | 154 |

    Тут никаких проблем возникнуть не должно. Теперь добавим разметку отдельной новости:

    155 | 156 | ```html 157 |
    158 |

    Новости

    159 | 160 |
      161 |
    • 162 |

      Заголовок новости

      163 |

      Текст новости

      164 |
    • 165 |
    • 166 |
    167 |
    168 | ``` 169 | 170 |

    Нам нужно добавить класс заголовку новости. Первым делом приходит в голову .news__title, но такой класс уже занят. Предположим, что второй элемент будет не .title, а .subject, тогда в CSS получается такое:

    171 | 172 | ```css 173 | .news__title { ... } 174 | .news__subject { ... } 175 | ``` 176 | 177 |

    Без дополнительных комментариев будет совершенно невозможно понять какой из них является заголовком всего блока, а какой — отдельной новости. Не пойдёт.

    178 | 179 |

    Следующий вариант — .news__item__title, но в БЭМ нельзя создавать элемент элемента, и это понятно, потому что получается каша. Ещё вариант: .news__item-title — тоже не годится, потому что может быть неочевидным как title соотносится с item. Как же быть?

    180 | 181 |

    Решение простое: на уровне элемента .news__item можно объявить новый блок (например, .news-item), и строить вложенные классы уже от него. Да, это не самостоятельный переиспользуемый блок, здесь объявление блока нужно только для того, чтобы разгрузить селекторы. Что получается:

    182 | 183 | ```html 184 |
    185 |

    Новости

    186 | 187 |
      188 |
    • 189 |

      Заголовок новости

      190 |

      Текст новости

      191 |
    • 192 |
    • 193 |
    194 |
    195 | ``` 196 | 197 |

    Проблема решена: нам больше не нужно использовать монструозные классы, при этом класс точно описывает элемент, и в CSS будет сразу понятно какой класс за что отвечает:

    198 | 199 | ```css 200 | .news__title { ... } 201 | .news-item__title { ... } 202 | ``` 203 | 204 |

    Простой и удобный выход из неудобной ситуации.

    205 | 206 |
  6. 207 |
208 | 209 |

Больше примеров разметки можно увидеть здесь.

210 | 211 |

Ещё одно хорошее руководство по использованию БЭМ есть здесь.

212 | -------------------------------------------------------------------------------- /_posts/accessibility.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Доступность 3 | order: 7 4 | 5 | links: 6 | - url: '#accessibility-important' 7 | text: 'Доступность — это важно' 8 | - url: '#accessibility-simple' 9 | text: 'Доступность — это просто' 10 | - url: '#examples' 11 | text: 'Покажите мне всё' 12 | 13 | additional_links: 14 | - url: 'https://youtu.be/atXxkKjPbN8' 15 | text: 'Зачем нужны заголовки — HTML Шорты' 16 | - url: 'https://youtu.be/Ns0zijQJxH4' 17 | text: 'Как прятать — HTML Шорты' 18 | - url: 'http://css.yoksel.ru/inaccessibility' 19 | text: 'Недоступность в картинках: как скринридеры видят сайты' 20 | --- 21 | 22 |

Доступность — это очень важная вещь. Она не про слепых и зрячих, и не про здоровых и больных, это понятие гораздо шире, она про удобство пользования интерфейсом для всех. Любой человек может испытывать проблемы со здоровьем или просто оказаться в неудобных условиях, когда не получается пользоваться сайтом привычным способом.

23 | 24 |

Например, правша сломал правую руку и пытается двигать мышь левой — ему будет очень неудобно и непривычно пользоваться интерфейсом, с которым раньше не было никаких проблем. Или вы в болтающемся автобусе по пути на работу пытаетесь читать с телефона и не промахиваться по кнопкам. Или женщина с малышом на руках пытается одновременно удержать малыша и набрать что-то одним пальцем — будут проблемы одновременно и с вниманием, и с координацией движений.

25 | 26 |

Безусловно, есть люди, которые в силу состояния своего здоровья постоянно испытывают такие затруднения, но также есть и другие, для которых это состояние временное, и таких людей гораздо больше. Иногда это мы сами. Поэтому доступность — это важно.

27 | 28 |

Доступность — это просто

29 | 30 |

Обеспечить доступность на самом элементарном уровне можно с помощью семантической разметки, то есть используя теги по смыслу. Это важно по двум причинам:

31 | 32 |
    33 |
  1. Если не загрузятся стили, пользователь получит не кашу из текста и картинок, а понятную страницу, которую без труда сможет прочитать.
  2. 34 | 35 |
  3. Скринридеры смогут различить и прочитать все элементы на странице. Встретив ссылку, они скажут, что это ссылка и на неё можно нажать, список зачитают как список, а не как набор разрозненных тегов, они прочитают описания для картинок и построят структуру страницы используя заголовки.
  4. 36 |
37 | 38 |

И всё это просто за счёт использования тегов по назначению.

39 | 40 |

Вот здесь можно почитать как скринридеры видят сайты и что они могут там не найти, если страница свёрстана плохо.

41 | 42 |

Покажите мне всё

43 | 44 |

Самые простые примеры:

45 | 46 |
47 |
48 |
49 |

Если нужно сделать кликабельный элемент, выбирайте ссылку или кнопку. Выбирайте ссылку, если клик уводит на другую страницу, если нет — используйте кнопку. Скринридеры понимают эти элементы как активные, и могут озвучить это для пользователя. Если вместо кнопки используется div или span, скринридер не поймёт, что на него можно нажать.

50 | 51 |

Также, по возможности, делайте кликабельную область большого размера, даже если элемент визуально небольшой. Это особенно важно для мобильных версий, где мы кликаем пальцем, но и на широких экранах будет удобнее, если по кнопке или ссылке можно попасть не прицеливаясь.

52 | 53 |

54 |

По теме:

55 | 63 |

64 |
65 | 66 |

Заголовки

67 |
68 |

Теги заголовков h1-h6 нужны не только для красоты, но и для выстраивания структуры страницы, с их помощью можно сформировать иерархическое дерево документа с разделами и подразделами.

69 | 70 |

Выбор уровня заголовка на основе иерархии документа решает сразу две задачи:

71 | 72 |
    73 |
  1. Как верстальщику, вам не придётся ломать голову над тегом для заголовка: у соседних элементов уровень заголовков одинаковый, если у родителя заголовок h2, то у дочерних элементов должны быть заголовки h3, и так далее. 74 |
  2. 75 |
  3. Основываясь на заголовках скринридеры строят структуру страницы, по которой можно навигироваться, таким образом пользователи читалок могут сразу выбрать нужный раздел без необходимости читать весь текст — это как быстро найти нужную главу в оглавлении книги.
  4. 76 |
77 | 78 |

Иногда бывает, что на странице есть какой-то самостоятельный раздел, но по макету у него нет заголовка. Получается, что эта часть страницы не будет представлена в оглавлении, которым пользуются читалки. Проблему можно решить добавив заголовок, который затем будет скрыт с помощью CSS. Скрывать рекомендуется таким кодом (источник):

79 | 80 | ```css 81 | .visuallyhidden:not(:focus):not(:active) { 82 | position: absolute; 83 | width: 1px; 84 | height: 1px; 85 | margin: -1px; 86 | border: 0; 87 | padding: 0; 88 | white-space: nowrap; 89 | clip-path: inset(100%); 90 | clip: rect(0 0 0 0); 91 | overflow: hidden; 92 | } 93 | ``` 94 | 95 |

Такого заголовка не будет на странице, но скринридеры прочитают его без труда. Также это решает задачу с тем, что по спецификации у section и article должны быть заголовки: если они не предусмотрены по макету, просто добавьте скрытые.

96 | 97 |

98 |

По теме:

99 | 107 |

108 |
109 | 110 |

Изображения

111 | 112 |
113 |

У всех img на странице должен быть указан атрибут alt: его могут прочитать скринридеры, его увидят пользователи с отключенными или незагрузившимися картинками.

114 | 115 |

Если в качестве контентного изображения используется инлайновый SVG, его содержимое можно сделать доступным для скринридеров, добавив role="img" aria-label="Описание картинки".

116 | 117 |

Если картинки используются как фон для текста, задайте таким блокам фоновый цвет, схожий с цветами фонового изображения. Если картинки не загрузятся (или отключены), текст окажется на констрастном фоне и его всё равно можно будет прочитать.

118 | 119 |

120 |

По теме:

121 | 132 |

133 |
134 | 135 |

Элементы формы

136 | 137 |
138 |

Для разметки формы обязательно используйте соответствующие элементы, например fieldset, legend, label, input и textarea.

139 | 140 |

Скринридеры понимают такие элементы. Они видят fieldset как группу инпутов, а legend — как название группы. Скринридер прочитает лейблы как названия для чекбоксов и полей, и пользователь сможет выбрать желаемые опции или ввести текст.

141 | 142 |

Без лейблов скринридер не сможет понять назначение инпутов, а без fieldset и legend — понять как группируются элементы формы и как они связаны между собой, и форма может оказаться полностью недоступной.

143 |
144 |
145 | 146 |

Как видите, здесь нет ничего сложного, и просто используя теги по смыслу, можно получить не только аккуратный осмысленный код, но и сделать страницу немного лучше с точки зрения доступности.

147 | 148 | 152 | -------------------------------------------------------------------------------- /_posts/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Примеры кода 3 | order: 3 4 | 5 | links: 6 | - text: 'Простой список' 7 | url: '#simple-list' 8 | - text: 'Картинка пользователя (юзерпик)' 9 | url: '#userpic' 10 | - text: 'Галерея' 11 | url: '#gallery' 12 | - text: 'Навигация (простой вариант)' 13 | url: '#nav-1' 14 | - text: 'Навигация (сложный вариант)' 15 | url: '#nav-2' 16 | - text: 'Виджет в боковой колонке' 17 | url: '#widget' 18 | - text: 'Блок новостей' 19 | url: '#news' 20 | - text: 'Статья или пост в блоге (простой вариант)' 21 | url: '#simple-article' 22 | - text: 'Статья или пост в блоге (сложный вариант)' 23 | url: '#article' 24 | - text: 'Разметка страницы' 25 | url: '#page-markup' 26 | --- 27 | 28 |
29 | Это примеры блоков, размеченных по описанным выше принципам, от простого к сложному. 30 |
31 | 32 |
    33 |
  1. 34 |

    Простой список

    35 | 36 | ```html 37 |
      38 |
    • Первое
    • 39 |
    • Второе
    • 40 |
    • Третье
    • 41 |
    42 | ``` 43 |
  2. 44 | 45 |
  3. 46 |

    Картинка пользователя (юзерпик)

    47 | 48 | ```html 49 | 53 | ``` 54 |
  4. 55 | 56 |
  5. 57 | 58 | 59 | ```html 60 | 70 | ``` 71 |
  6. 72 | 73 |
  7. 74 | 75 | 76 | ```html 77 | 84 | ``` 85 |
  8. 86 | 87 |
  9. 88 | 89 | 90 | ```html 91 | 107 | ``` 108 |
  10. 109 | 110 |
  11. 111 |

    Виджет в боковой колонке

    112 | 113 | ```html 114 |
    115 |

    Выращиваем желе

    116 | 117 |
    118 |

    Чтобы вырастить общительное дружелюбное желе, 119 | нам потребуется рулон поролона, два килограмма сахара, 120 | три яйца и пол чайной чашки ацетона.

    121 | 122 | Не читать дальше... 123 |
    124 |
    125 | ``` 126 |
  12. 127 | 128 |
  13. 129 |

    Блок новостей

    130 | 131 | ```html 132 |
    133 |

    Вчерашние новости

    134 | 135 |
      136 | 138 |
    • 139 |

      140 | Соревнования среди воблы по конькобежному спорту 141 |

      142 |
      143 |

      Победила команда килек из Петрозаводска

      144 | 145 | Читать дальше 146 |
      147 |
    • 148 | 149 |
    • 150 |

      151 | Учёные уточнили роль напильника в уходе за ногтями 152 |

      153 |
      154 |

      Британские учёные высоко оценили вклад 155 | напильника в отращивание полутораметровых ногтей.

      156 | 157 | Не читать дальше 158 |
      159 |
    • 160 |
    161 |
    162 | ``` 163 |
  14. 164 | 165 |
  15. 166 |

    Статья или пост в блоге (простой вариант)

    167 | 168 | ```html 169 |
    170 |

    171 | Нащупываем чакры у пучка петрушки 172 |

    173 | 174 | 175 | 180 | 181 |
    182 | Сходите на рынок и купите у старушек пучок петрушки грамм на 100. 183 | Как следует переберите, очистите от жуков и гусениц. Жуков отдайте поиграться 184 | коту, гусениц поселите в горшок с кактусами, пусть одна будет Джоном, 185 | вторая Билли, а у вас в горшке теперь будет Дикий Запад. Вернитесь 186 | к пучку петрушки. Ласково взгляните на него и как следует почешите 187 | за ухом, можно себе или коту. Перевяжите атласной ленточкой, 188 | непременно завяжите бант. Поздравляем! Теперь у вас есть полностью 189 | одомашненный пучок петрушки, который будет весело бегать за вами 190 | по пятам и проращивать свои семена в ваших тапках. 191 |
    192 |
    193 | ``` 194 |
  16. 195 | 196 |
  17. 197 |

    Статья или пост в блоге (сложный вариант)

    198 | 199 | ```html 200 |
    201 |
    202 |

    203 | 204 | Резиновые уточки как способ самопознания 205 | 206 |

    207 | 208 | 209 |
    210 | 211 | 216 | 217 |
    218 | Достаньте с чердака коробку с полусотней резиновых уточек, 219 | оставшихся после празднования нового года. Из уточек 220 | и горящих свечей выложите пентаграмму на полу комнаты. 221 | Сядьте посередине в позу лотоса, в каждую руку возьмите 222 | по немецко-бразильскому словарю, прокашляйтесь, наберите 223 | полную грудь воздуха и громко и уверенно, 224 | с полной самоотдачей скажите "Кря!" 225 |
    226 | 227 | 242 | 243 | 254 |
    255 | ``` 256 |
  18. 257 | 258 |
  19. 259 |

    Разметка страницы

    260 | 261 | ```html 262 | 263 | 272 | 273 | 274 |
    275 | 276 |
    277 |

    Преимущества

    278 | 279 |
      280 |
    • Высокие потолки
    • 281 | [...] 282 |
    283 |
    284 | 285 |
    286 |

    Тарифы

    287 | 288 |
      289 |
    • 30 зонтиков в минуту
    • 290 | [...] 291 |
    292 |
    293 |
    294 | 295 | 296 | 302 | 303 | 304 | 315 | ``` 316 |
  20. 317 |
318 | --------------------------------------------------------------------------------