├── .eleventy.js ├── .gitignore ├── README.md ├── app └── lastAccessed.js ├── feedDataFormat.js ├── package-lock.json ├── package.json └── src ├── _data ├── dateLimit.js ├── feeds.js ├── lastAccessed.js └── meta.js ├── _includes ├── base.njk ├── postDate.njk └── sourceCard.njk ├── feeds ├── css.json ├── eleventy.json ├── paginate │ ├── categories.njk │ ├── items.njk │ └── sources.njk └── podcasts.json ├── index.njk └── sass ├── _card.scss ├── _item-articles.scss ├── _layout.scss ├── _prismtheme.scss ├── _reset.scss ├── _theme.scss ├── _utilities.scss └── style.scss /.eleventy.js: -------------------------------------------------------------------------------- 1 | const { DateTime } = require("luxon"); 2 | const slugify = require("slugify"); 3 | const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight"); 4 | 5 | module.exports = function (eleventyConfig) { 6 | eleventyConfig.addPlugin(syntaxHighlight); 7 | 8 | eleventyConfig.addWatchTarget("./src/sass/"); 9 | 10 | eleventyConfig.addFilter("slug", (str) => { 11 | return slugify(str, { 12 | lower: true, 13 | strict: true, 14 | remove: /["]/g, 15 | }); 16 | }); 17 | 18 | eleventyConfig.addFilter("postDate", (dateObj) => { 19 | return DateTime.fromJSDate(dateObj).toLocaleString(DateTime.DATE_MED); 20 | }); 21 | 22 | eleventyConfig.addShortcode("newCount", (items, source, slug) => { 23 | const newItems = items.filter((i) => { 24 | return i.data.source === source && i.data.new === "true"; 25 | }); 26 | 27 | return newItems.length > 0 28 | ? `${newItems.length} new items` 29 | : ""; 30 | }); 31 | 32 | eleventyConfig.addFilter("limit", function (arr, limit) { 33 | return arr.slice(0, limit); 34 | }); 35 | 36 | eleventyConfig.addFilter("domain", function (url) { 37 | const domain = new URL(url); 38 | 39 | return domain.hostname.replace("www.", ""); 40 | }); 41 | 42 | eleventyConfig.addFilter("dateLimitDisplay", (dateObj) => { 43 | return DateTime.fromMillis(dateObj).toLocaleString(DateTime.DATE_MED); 44 | }); 45 | 46 | eleventyConfig.addFilter("stripUnsafe", (content) => { 47 | const regex = //gim; 48 | return content != undefined ? content.replace(regex, " ") : content; 49 | }); 50 | 51 | return { 52 | dir: { 53 | input: "src", 54 | output: "public", 55 | }, 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies installed by npm 2 | node_modules 3 | 4 | # build artefacts 5 | public 6 | .vscode 7 | 8 | # secrets and errors 9 | .env 10 | .log 11 | 12 | # macOS related files 13 | .DS_Store 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Preview of the Eleventy RSS Reader starter](https://repository-images.githubusercontent.com/373531262/91192f80-d222-11eb-8bcb-325ff3d0a3e7) 2 | 3 | # Eleventy RSS Reader Starter 4 | 5 | > We built most of this project LIVE over on Twitch. Visit the [announcement post](https://dev.to/5t3ph/let-s-build-a-jamstack-app-together-5hkp) to learn more. 6 | 7 | This repo is now ready as an Eleventy starter! Review the recordings and customization section below to learn more about how it was built and how it works. 8 | 9 | [Subscribe to my newsletter](https://moderncss.dev) to receive my weekly streaming schedule (and other news about my various projects + CSS tips). 10 | 11 | [Follow @5t3ph on Twitter](https://twitter.com/5t3ph) 12 | 13 | ## Available Recordings 14 | 15 | > Final edited recordings will eventually be published to the [11ty Rocks YouTube Channel](https://www.youtube.com/channel/UCTuSQg_Ol4shhSYQ1EfpHiQ?sub_confirmation=1) 16 | 17 | 📺 = moved to YouTube 18 | 19 | - 📺 [Project intro and creating the feature list](https://youtu.be/ADx7RbtIWwg) 20 | - 📺 [Begin the 11ty architecture + add feedparser](https://youtu.be/Dju1X7YNYzk) 21 | - 📺 [Reorganize, adjust feed output, and create dynamic views](https://youtu.be/tMgoOsecjLw) 22 | - 📺 [Enable method to determine "new", link views](https://youtu.be/tLL4offqbTo) 23 | - 📺 [Filters and templating](https://youtu.be/C4fye6K1IiQ) 24 | - Part one of [CSS styling](https://www.twitch.tv/videos/1058997704?collection=G7YXMEt6hhYCyw) 25 | - Part two of [CSS styling](https://www.twitch.tv/videos/1059018865?collection=G7YXMEt6hhYCyw) 26 | 27 | > Note that additional styling and further organization was completed outside of the streams to get the starter fully release-ready. Check the commit history if you're interested in the difference between streamed dev and post-stream dev. 28 | 29 | ## Customization 30 | 31 | ### Site Title 32 | 33 | Edit `src/_data/meta.js` to update the `siteTitle` value. 34 | 35 | ### Colors 36 | 37 | Update the CSS custom properties values within `src/sass/_theme.scss` to quickly retheme the app. 38 | 39 | ### RSS Sources 40 | 41 | Modify, add, or remove the JSON files within `src/feeds/` following the schema of: 42 | 43 | ```json 44 | { 45 | "category": "Category Name", 46 | "items": ["https://permalink.to/feed"] 47 | } 48 | ``` 49 | 50 | ## Development Scripts 51 | 52 | **`npm start`** 53 | 54 | > Run 11ty with hot reload at localhost:8080, including reload based on Sass changes 55 | 56 | **`npm run build`** 57 | 58 | > Production build includes minified, autoprefixed CSS 59 | 60 | Use this as the "Publish command" if needed by hosting such as Netlify. 61 | 62 | ## Resources to extend this and learn 11ty 63 | 64 | **New to Eleventy?** Get started with my [written tutorial for beginners](https://11ty.rocks/posts/create-your-first-basic-11ty-website/) 65 | 66 | **Learn to build an 11ty site in 20 mins** with my [egghead video course](https://5t3ph.dev/learn-11ty) and see how to add a blog and custom data. 67 | 68 | **Explore advanced setup of custom data** through my [tutorial on building a community site](https://css-tricks.com/a-community-driven-site-with-eleventy-building-the-site/) 69 | 70 | **Even more resources** are available from [11ty.Rocks](https://11ty.rocks) 71 | -------------------------------------------------------------------------------- /app/lastAccessed.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const fs = require("fs"); 3 | 4 | const env = process.env.ELEVENTY_ENV; 5 | 6 | const d = new Date(); 7 | 8 | // For local dev, set date back X days 9 | if (env === "dev") { 10 | // Range of your choosing 11 | d.setDate(d.getDate() - 7); 12 | } 13 | 14 | const time = new Date(d).getTime(); 15 | 16 | try { 17 | fs.writeFileSync("./src/_data/lastAccessed.js", `module.exports = ${time};`); 18 | } catch (err) { 19 | console.error(err); 20 | } 21 | -------------------------------------------------------------------------------- /feedDataFormat.js: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | category: "CSS", 4 | items: [ 5 | { 6 | feedTitle: "11ty Rocks!", 7 | items: [ 8 | { 9 | feedTitle: "11ty Rocks!", 10 | feedAuthor: "Stephanie Eckles", 11 | siteUrl: "https://11ty.rocks/", 12 | feedUrl: "https://11ty.rocks/feed/", 13 | title: ".eleventy.js Config Samples", 14 | date: "2020-11-23T00: 00: 00.000Z", 15 | excerpt: 16 | "A collection of filters, shortcodes, and other tips for extending 11ty, such as working with dates and extending built-in filters.\nView All Samples...", 17 | content: 18 | '

A collection of filters, shortcodes, and other tips for extending 11ty, such as working with dates and extending built-in filters.

\nView All Samples', 19 | link: "https://11ty.rocks/eleventyjs/", 20 | }, 21 | ], 22 | }, 23 | ], 24 | }, 25 | ]; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "11ty-rss-reader", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "watch:sass": "sass --no-source-map --watch src/sass:public/css", 8 | "watch:eleventy": "eleventy --serve", 9 | "build:sass": "sass --no-source-map src/sass:public/css", 10 | "build:eleventy": "eleventy", 11 | "css": "postcss public/css/*.css -u autoprefixer cssnano -r --no-map", 12 | "lastaccess": "node app/lastAccessed.js", 13 | "prestart": "cross-env ELEVENTY_ENV=dev npm run lastaccess", 14 | "postbuild": "cross-env ELEVENTY_ENV=prod npm-run-all --parallel css lastaccess", 15 | "start": "npm-run-all build:sass --parallel watch:*", 16 | "build": "npm-run-all build:sass build:eleventy" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/5t3ph/11ty-sass-skeleton.git" 21 | }, 22 | "author": "5t3ph", 23 | "license": "ISC", 24 | "dependencies": { 25 | "@11ty/eleventy": "^0.12.1", 26 | "@11ty/eleventy-plugin-syntaxhighlight": "^3.0.6", 27 | "autoprefixer": "^10.2.6", 28 | "cross-env": "^7.0.3", 29 | "cssnano": "^5.0.6", 30 | "dotenv": "^10.0.0", 31 | "feedparser": "^2.2.10", 32 | "node-fetch": "^2.6.1", 33 | "npm-run-all": "^4.1.5", 34 | "postcss": "^8.3.5", 35 | "postcss-cli": "^8.3.1", 36 | "sass": "^1.35.1" 37 | }, 38 | "browserslist": [ 39 | "last 2 versions" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/_data/dateLimit.js: -------------------------------------------------------------------------------- 1 | const d = new Date(); 2 | d.setDate(d.getDate() - 120); 3 | 4 | module.exports = new Date(d).getTime(); 5 | -------------------------------------------------------------------------------- /src/_data/feeds.js: -------------------------------------------------------------------------------- 1 | // @link https://www.npmjs.com/package/feedparser 2 | const FeedParser = require("feedparser"); 3 | const fetch = require("node-fetch"); 4 | const fastglob = require("fast-glob"); 5 | const fs = require("fs"); 6 | const lastAccessed = require("./lastAccessed.js"); 7 | const dateLimit = require("./dateLimit.js"); 8 | 9 | const getSources = async () => { 10 | // Create a "glob" of all feed json files 11 | const feedFiles = await fastglob("./src/feeds/*.json", { 12 | caseSensitiveMatch: false, 13 | }); 14 | 15 | // Loop through those files and add their content to our `feeds` Set 16 | let feeds = []; 17 | for (let feed of feedFiles) { 18 | const feedData = JSON.parse(fs.readFileSync(feed)); 19 | feeds.push(feedData); 20 | } 21 | 22 | // Return the feeds Set of objects within an array 23 | return feeds; 24 | }; 25 | 26 | const createExcerpt = (content) => { 27 | return ( 28 | content 29 | .replace(/(<([^>]+)>)/gi, "") 30 | .substr(0, content.lastIndexOf(" ", 200)) 31 | .trim() + "..." 32 | ); 33 | }; 34 | 35 | const parseFeed = async (feed, category) => { 36 | return await new Promise((resolve) => { 37 | const req = fetch(feed); 38 | 39 | const feedparser = new FeedParser(); 40 | let feedItems = []; 41 | 42 | req.then( 43 | function (res) { 44 | if (res.status !== 200) { 45 | throw new Error("Bad status code"); 46 | } else { 47 | // The response `body` -- res.body -- is a stream 48 | res.body.pipe(feedparser); 49 | } 50 | }, 51 | function (_err) { 52 | // handle any request errors 53 | } 54 | ); 55 | 56 | feedparser.on("error", function (_error) { 57 | // always handle errors 58 | }); 59 | 60 | feedparser.on("readable", function () { 61 | let stream = this; 62 | let item; 63 | 64 | while ((item = stream.read())) { 65 | let feedItem = { category }; 66 | const itemTimestamp = new Date(item["date"]).getTime(); 67 | 68 | if (itemTimestamp < dateLimit) { 69 | return; 70 | } 71 | 72 | // Process feedItem item and push it to items data if it exists 73 | if (item["title"] && item["date"]) { 74 | // Feed Source meta data 75 | feedItem["feedTitle"] = item["meta"].title; 76 | feedItem["feedAuthor"] = item["meta"].author || item["author"]; 77 | feedItem["siteUrl"] = item["meta"].link; 78 | feedItem["feedUrl"] = item["meta"].xmlurl; 79 | 80 | // Check freshness 81 | if (itemTimestamp > lastAccessed) { 82 | feedItem["new"] = "true"; 83 | } 84 | 85 | // Individual item data 86 | feedItem["title"] = item["title"]; 87 | feedItem["date"] = item["date"]; 88 | 89 | if (item["summary"]) { 90 | feedItem["excerpt"] = createExcerpt(item["summary"]); 91 | } else if (item["description"]) { 92 | feedItem["excerpt"] = createExcerpt(item["description"]); 93 | } 94 | 95 | if (item["description"]) { 96 | feedItem["content"] = item["description"]; 97 | } 98 | 99 | if (item["link"]) { 100 | feedItem["link"] = item["link"]; 101 | } 102 | 103 | if (item["image"] && item["image"].url) { 104 | feedItem["imageUrl"] = item["image"].url; 105 | } 106 | 107 | feedItems.push(feedItem); 108 | } 109 | } 110 | }); 111 | 112 | feedparser.on("end", function () { 113 | resolve(feedItems); 114 | }); 115 | }).catch(() => { 116 | return Promise.resolve([]); 117 | }); 118 | }; 119 | 120 | module.exports = async () => { 121 | const feedSources = await getSources(); 122 | 123 | const feeds = []; 124 | for (const categorySource of feedSources) { 125 | const { category } = categorySource; 126 | const items = []; 127 | 128 | for (const feed of categorySource.items) { 129 | const feedData = await parseFeed(feed, category); 130 | if (feedData.length) { 131 | items.push({ 132 | feedTitle: feedData[0].feedTitle, 133 | siteUrl: feedData[0].siteUrl, 134 | category, 135 | items: feedData, 136 | }); 137 | } 138 | } 139 | 140 | feeds.push({ category, items }); 141 | } 142 | 143 | return feeds; 144 | }; 145 | -------------------------------------------------------------------------------- /src/_data/lastAccessed.js: -------------------------------------------------------------------------------- 1 | module.exports = 1619066469258; -------------------------------------------------------------------------------- /src/_data/meta.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "siteTitle": "Steph's Favorite RSS" 3 | } -------------------------------------------------------------------------------- /src/_includes/base.njk: -------------------------------------------------------------------------------- 1 | {%- set itemCategory %} 2 | {% if category %} 3 | {{ category }} 4 | {% endif %} 5 | {% endset -%} 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{ title }} 13 | 14 | 15 | 16 | {% if page.fileSlug %} 17 | 21 | {% endif %} 22 | 23 |
24 | {% if siteUrl and not link %} 25 |
26 | {% newCount collections.items, title, title | slug %} 27 |
28 |

{{ title }}

29 |

{{ itemCategory | safe }} Visit full site

30 |
31 | {% elseif link %} 32 |
33 | {% include "postDate.njk" %} 34 |

{{ title }}

35 |

{{ itemCategory | safe }} All from {{ item.feedTitle }} View {{ title }} on {{ item.siteUrl | domain }}

36 | {% elseif not page.fileSlug %} 37 |
38 |

{{ meta.siteTitle }}

39 | Fetching results since {{ dateLimit | dateLimitDisplay }} 40 | {% else %} 41 |
42 |

{{ title }}

43 | {% endif %} 44 |
45 |
46 |
47 | {{ content | safe }} 48 |
49 | 52 | 53 | {% if link %} 54 | 55 | {% endif %} 56 | 57 | -------------------------------------------------------------------------------- /src/_includes/postDate.njk: -------------------------------------------------------------------------------- 1 | {% if item.new %}New {% endif %}{{ item.date | postDate }} 2 | -------------------------------------------------------------------------------- /src/_includes/sourceCard.njk: -------------------------------------------------------------------------------- 1 |
  • 2 | {% set feedSlug %}{{ feed.feedTitle | slug }}{% endset %} 3 | 4 |
    5 | {% newCount collections.items, feed.feedTitle, feedSlug %} 6 |

    {{ feed.feedTitle }}

    7 |
    8 | 16 | 19 |
  • -------------------------------------------------------------------------------- /src/feeds/css.json: -------------------------------------------------------------------------------- 1 | { 2 | "category": "CSS", 3 | "items": [ 4 | "https://css-tricks.com/author/stephanieeckles/feed/", 5 | "https://moderncss.dev/feed/", 6 | "https://smolcss.dev/feed/", 7 | "https://css-irl.info/rss.xml", 8 | "https://piccalil.li/feed.xml", 9 | "https://www.sarasoueidan.com/blog/index.xml", 10 | "https://stephaniewalter.design/feed/", 11 | "http://ishadeed.com/feed.xml", 12 | "https://mxb.at/feed.xml" 13 | ] 14 | } -------------------------------------------------------------------------------- /src/feeds/eleventy.json: -------------------------------------------------------------------------------- 1 | { 2 | "category": "Eleventy", 3 | "items": ["https://11ty.rocks/feed/"] 4 | } -------------------------------------------------------------------------------- /src/feeds/paginate/categories.njk: -------------------------------------------------------------------------------- 1 | --- 2 | pagination: 3 | data: feeds 4 | size: 1 5 | alias: categories 6 | permalink: "/category/{{ categories.category | slug }}/" 7 | layout: base.njk 8 | eleventyComputed: 9 | title: "{{ categories.category }}" 10 | --- 11 | 12 | -------------------------------------------------------------------------------- /src/feeds/paginate/items.njk: -------------------------------------------------------------------------------- 1 | ---js 2 | { 3 | pagination: { 4 | data: "feeds", 5 | size: 1, 6 | alias: "item", 7 | addAllPagesToCollections: true, 8 | before: function(data) { 9 | const feedItems = []; 10 | 11 | for (const category of data) { 12 | for (const source of category.items) { 13 | for (const item of source.items) { 14 | feedItems.push(item); 15 | } 16 | } 17 | } 18 | 19 | return feedItems; 20 | } 21 | }, 22 | layout: "base.njk", 23 | tags: "items", 24 | permalink: "/{{ item.feedTitle | slug }}/{{ item.title | slug }}/", 25 | templateEngineOverride: "md, njk", 26 | eleventyComputed: { 27 | title: "{{ item.title }}", 28 | new: "{{ item.new }}", 29 | source: "{{ item.feedTitle }}", 30 | link: "{{ item.link }}", 31 | category: "{{ item.category }}", 32 | siteUrl: "{{ item.siteUrl }}" 33 | } 34 | } 35 | --- 36 |
    37 | {% if item.content %} 38 | {{ item.content | stripUnsafe | safe }} 39 | {% else %} 40 |

    No content found - view {{ title }} on {{ item.siteUrl | domain }}

    41 | {% endif %} 42 |
    -------------------------------------------------------------------------------- /src/feeds/paginate/sources.njk: -------------------------------------------------------------------------------- 1 | ---js 2 | { 3 | pagination: { 4 | data: "feeds", 5 | size: 1, 6 | alias: "sources", 7 | before: function(data) { 8 | const sourceItems = []; 9 | 10 | for (const category of data) { 11 | for (const source of category.items) { 12 | sourceItems.push(source); 13 | } 14 | } 15 | 16 | return sourceItems; 17 | } 18 | }, 19 | layout: "base.njk", 20 | permalink: "/{{ sources.feedTitle | slug }}/", 21 | eleventyComputed: { 22 | title: "{{ sources.feedTitle }}", 23 | siteUrl: "{{ sources.siteUrl }}", 24 | category: "{{ sources.category }}" 25 | } 26 | } 27 | --- 28 | -------------------------------------------------------------------------------- /src/feeds/podcasts.json: -------------------------------------------------------------------------------- 1 | { 2 | "category": "Podcasts", 3 | "items": [ 4 | "https://wordwrap.dev/feed/", 5 | "https://anchor.fm/s/3a915d90/podcast/rss", 6 | "https://www.omnycontent.com/d/playlist/aaea4e69-af51-495e-afc9-a9760146922b/75a86d39-9e0e-4e9a-b948-aae301805fe6/514362cf-31b4-4ed2-af40-aae301805ffd/podcast.rss" 7 | ] 8 | } -------------------------------------------------------------------------------- /src/index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.njk 3 | title: Feeds 4 | --- 5 | 6 | {# #} 11 | 12 | {% for category in feeds %} 13 |

    {{ category.category }}

    14 | 19 | {% endfor %} -------------------------------------------------------------------------------- /src/sass/_card.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | --card-padding: 1.5rem; 3 | 4 | display: grid; 5 | gap: 1.5rem; 6 | grid-auto-rows: auto 1fr auto; 7 | align-items: start; 8 | padding-top: var(--card-padding); 9 | border-radius: 0.5rem; 10 | overflow: hidden; 11 | box-shadow: 0 0.25rem 0.5rem -0.05rem hsla(var(--color-primary-hs), 15%, 0.3); 12 | 13 | &-excerpt { 14 | gap: 0.5rem; 15 | grid-auto-rows: auto auto 1fr auto; 16 | 17 | footer { 18 | margin-top: 1rem; 19 | } 20 | } 21 | 22 | header { 23 | display: grid; 24 | grid-template-columns: auto 1fr; 25 | gap: 1rem; 26 | 27 | h3 { 28 | margin-top: 0.5ex; 29 | } 30 | } 31 | 32 | h2 { 33 | font-size: 1.35rem; 34 | } 35 | 36 | > *:not(footer) { 37 | padding-right: var(--card-padding); 38 | padding-left: var(--card-padding); 39 | 40 | a { 41 | text-decoration: none; 42 | font-size: 1.35rem; 43 | } 44 | } 45 | 46 | ul { 47 | margin: 0; 48 | display: grid; 49 | gap: 1rem; 50 | 51 | li { 52 | display: grid; 53 | gap: 0.25rem; 54 | } 55 | } 56 | 57 | footer { 58 | align-self: end; 59 | padding: 0.25em var(--card-padding); 60 | text-align: center; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/sass/_item-articles.scss: -------------------------------------------------------------------------------- 1 | .item-article { 2 | > * + * { 3 | margin-top: 1.5em; 4 | 5 | :is(h1, h2, h3) { 6 | margin-top: 3em; 7 | } 8 | } 9 | 10 | button { 11 | border: 2px solid; 12 | background: transparent; 13 | border-radius: 4px; 14 | color: var(--color-primary); 15 | cursor: pointer; 16 | } 17 | 18 | blockquote, 19 | figure, 20 | figcaption { 21 | display: grid; 22 | grid-gap: 1rem; 23 | } 24 | 25 | blockquote { 26 | padding: 0.5em 0.5em 0.5em 1.5em; 27 | border-left: 2px solid var(--color-secondary); 28 | background-color: var(--color-secondary-light); 29 | } 30 | 31 | figure { 32 | justify-content: center; 33 | } 34 | 35 | figcaption { 36 | font-size: 0.9rem; 37 | } 38 | 39 | ::marker { 40 | color: var(--color-secondary); 41 | } 42 | 43 | mark { 44 | background-color: var(--color-secondary-light); 45 | } 46 | 47 | #carbonads { 48 | display: none; 49 | } 50 | 51 | hr { 52 | border: none; 53 | border-top: 1px solid var(--color-secondary); 54 | margin: 8vh; 55 | } 56 | 57 | img[src^="/"], 58 | img[src^="."] { 59 | padding: 1rem; 60 | font-size: 0.9rem; 61 | max-width: 80%; 62 | border: 2px solid var(--color-secondary); 63 | margin-left: auto; 64 | margin-right: auto; 65 | border-radius: 0.5rem; 66 | box-shadow: 0 0 0 0.5rem var(--color-secondary-light); 67 | } 68 | 69 | iframe[src*="vimeo"], 70 | iframe[src*="youtube"] { 71 | aspect-ratio: 16/9; 72 | max-width: 100%; 73 | } 74 | 75 | *[style*="padding"], 76 | *[style*="margin"] { 77 | padding: unset !important; 78 | margin: unset !important; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/sass/_layout.scss: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-direction: column; 4 | min-height: 100vh; 5 | font-family: system-ui, sans-serif; 6 | font-size: 1.05rem; 7 | background-color: var(--background); 8 | color: var(--text); 9 | } 10 | 11 | a { 12 | color: var(--color-primary); 13 | 14 | &:focus { 15 | outline-offset: max(2px, 0.15em); 16 | outline: max(2px, 0.15em) dashed currentColor; 17 | scroll-margin-bottom: max(8vh, 2rem); 18 | } 19 | } 20 | 21 | h1 { 22 | font-size: 2.5rem; 23 | font-size: clamp(1.25rem, 4vw + 1rem, 3rem); 24 | } 25 | 26 | h2 { 27 | font-size: 2rem; 28 | letter-spacing: 0.02em; 29 | 30 | a { 31 | text-decoration-style: wavy; 32 | text-underline-offset: 0.2em; 33 | } 34 | } 35 | 36 | :is(h1, h2) { 37 | line-height: 1.3; 38 | } 39 | 40 | nav, 41 | body > footer { 42 | display: grid; 43 | grid-auto-flow: column; 44 | align-items: center; 45 | justify-content: space-around; 46 | gap: 2rem; 47 | text-align: center; 48 | padding: 0.5rem 1rem; 49 | 50 | > * { 51 | width: fit-content; 52 | } 53 | } 54 | 55 | body > header { 56 | min-height: 30vh; 57 | display: grid; 58 | place-content: center; 59 | text-align: center; 60 | padding-top: 5vh; 61 | padding-bottom: 5vh; 62 | } 63 | 64 | main { 65 | width: min(120ch, 100vw - 3rem); 66 | margin: 8vh auto; 67 | 68 | > * + * { 69 | margin-top: 1em; 70 | 71 | &:is(h2, h3) { 72 | margin-top: 2em; 73 | } 74 | } 75 | } 76 | 77 | main > h2:not(:first-of-type) { 78 | margin-top: 8vh; 79 | } 80 | 81 | body > footer { 82 | margin-top: auto; 83 | } 84 | 85 | article { 86 | padding: clamp(1rem, 5%, 3rem); 87 | border-radius: 0.25rem; 88 | } 89 | 90 | .container { 91 | margin-left: auto; 92 | margin-right: auto; 93 | width: min(80ch, 100vw - 3rem); 94 | 95 | &--source { 96 | display: grid; 97 | grid-auto-flow: column; 98 | align-items: center; 99 | justify-content: center; 100 | text-align: left; 101 | gap: 2rem; 102 | } 103 | } 104 | 105 | .header-meta { 106 | display: inline-flex; 107 | gap: 2rem; 108 | flex-wrap: wrap; 109 | margin-top: 1rem; 110 | text-align: center; 111 | justify-content: center; 112 | 113 | .category { 114 | display: inline-flex; 115 | gap: 0.25em; 116 | } 117 | } 118 | 119 | .grid { 120 | --min: 30ch; 121 | 122 | list-style: none; 123 | padding: 0; 124 | margin: 2rem 0 0; 125 | display: grid; 126 | gap: 2rem; 127 | grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--min)), 1fr)); 128 | } 129 | -------------------------------------------------------------------------------- /src/sass/_prismtheme.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * Copyright (c) 2018 Sarah Drasner 4 | * Sarah Drasner's[@sdras] Night Owl 5 | * Ported by Sara vieria [@SaraVieira] 6 | * Added by Souvik Mandal [@SimpleIndian] 7 | */ 8 | 9 | code:not([class]) { 10 | font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; 11 | color: var(--color-secondary); 12 | font-size: 1.75ex; 13 | } 14 | 15 | code[class*="language-"], 16 | pre, 17 | pre code:not([class]) { 18 | color: #d6deeb; 19 | font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; 20 | text-align: left; 21 | white-space: pre; 22 | word-spacing: normal; 23 | word-break: normal; 24 | word-wrap: normal; 25 | line-height: 1.5; 26 | font-size: 1em; 27 | 28 | -moz-tab-size: 4; 29 | -o-tab-size: 4; 30 | tab-size: 4; 31 | 32 | -webkit-hyphens: none; 33 | -moz-hyphens: none; 34 | -ms-hyphens: none; 35 | hyphens: none; 36 | } 37 | 38 | pre::-moz-selection, 39 | pre ::-moz-selection, 40 | code[class*="language-"]::-moz-selection, 41 | code[class*="language-"] ::-moz-selection { 42 | text-shadow: none; 43 | background: rgba(29, 59, 83, 0.99); 44 | } 45 | 46 | pre::selection, 47 | pre ::selection, 48 | code[class*="language-"]::selection, 49 | code[class*="language-"] ::selection { 50 | text-shadow: none; 51 | background: rgba(29, 59, 83, 0.99); 52 | } 53 | 54 | @media print { 55 | code[class*="language-"], 56 | pre { 57 | text-shadow: none; 58 | } 59 | } 60 | 61 | /* Code blocks */ 62 | pre { 63 | padding: 1em; 64 | margin-bottom: 0; 65 | overflow: auto; 66 | } 67 | 68 | :not(pre) > code[class*="language-"], 69 | pre { 70 | color: white; 71 | background: #011627; 72 | } 73 | 74 | :not(pre) > code[class*="language-"] { 75 | padding: 0.1em; 76 | border-radius: 0.3em; 77 | white-space: normal; 78 | } 79 | 80 | .token.comment, 81 | .token.prolog, 82 | .token.cdata { 83 | color: rgb(99, 119, 119); 84 | font-style: italic; 85 | } 86 | 87 | .token.punctuation { 88 | color: rgb(199, 146, 234); 89 | } 90 | 91 | .namespace { 92 | color: rgb(178, 204, 214); 93 | } 94 | 95 | .token.deleted { 96 | color: rgba(239, 83, 80, 0.56); 97 | font-style: italic; 98 | } 99 | 100 | .token.symbol, 101 | .token.property { 102 | color: rgb(128, 203, 196); 103 | } 104 | 105 | .token.tag, 106 | .token.operator, 107 | .token.keyword { 108 | color: rgb(127, 219, 202); 109 | } 110 | 111 | .token.boolean { 112 | color: rgb(255, 88, 116); 113 | } 114 | 115 | .token.number { 116 | color: rgb(247, 140, 108); 117 | } 118 | 119 | .token.constant, 120 | .token.function, 121 | .token.builtin, 122 | .token.char { 123 | color: rgb(130, 170, 255); 124 | } 125 | 126 | .token.selector, 127 | .token.doctype { 128 | color: rgb(199, 146, 234); 129 | font-style: italic; 130 | } 131 | 132 | .token.attr-name, 133 | .token.inserted { 134 | color: rgb(173, 219, 103); 135 | font-style: italic; 136 | } 137 | 138 | .token.string, 139 | .token.url, 140 | .token.entity, 141 | .language-css .token.string, 142 | .style .token.string { 143 | color: rgb(173, 219, 103); 144 | } 145 | 146 | .token.class-name, 147 | .token.atrule, 148 | .token.attr-value { 149 | color: rgb(255, 203, 139); 150 | } 151 | 152 | .token.regex, 153 | .token.important, 154 | .token.variable { 155 | color: rgb(214, 222, 235); 156 | } 157 | 158 | .token.important, 159 | .token.bold { 160 | font-weight: bold; 161 | } 162 | 163 | .token.italic { 164 | font-style: italic; 165 | } 166 | -------------------------------------------------------------------------------- /src/sass/_reset.scss: -------------------------------------------------------------------------------- 1 | /* Box sizing rules */ 2 | *, 3 | *::before, 4 | *::after { 5 | box-sizing: border-box; 6 | } 7 | 8 | /* Remove default margin */ 9 | body, 10 | h1, 11 | h2, 12 | h3, 13 | h4, 14 | p, 15 | figure, 16 | blockquote, 17 | dl, 18 | dd { 19 | margin: 0; 20 | } 21 | 22 | /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ 23 | ul[role="list"], 24 | ol[role="list"] { 25 | list-style: none; 26 | } 27 | 28 | /* Set core root defaults */ 29 | html:focus-within { 30 | scroll-behavior: smooth; 31 | } 32 | 33 | /* Set core body defaults */ 34 | body { 35 | min-height: 100vh; 36 | text-rendering: optimizeSpeed; 37 | line-height: 1.5; 38 | } 39 | 40 | /* A elements that don't have a class get default styles */ 41 | a:not([class]) { 42 | text-decoration-skip-ink: auto; 43 | } 44 | 45 | /* Make images easier to work with */ 46 | img, 47 | picture, 48 | svg { 49 | max-width: 100%; 50 | display: block; 51 | } 52 | 53 | svg { 54 | display: inline-block; 55 | fill: currentColor; 56 | width: 1em; 57 | vertical-align: middle; 58 | } 59 | 60 | /* Inherit fonts for inputs and buttons */ 61 | input, 62 | button, 63 | textarea, 64 | select { 65 | font: inherit; 66 | } 67 | 68 | /* Remove all animations, transitions and smooth scroll for people that prefer not to see them */ 69 | @media (prefers-reduced-motion: reduce) { 70 | html:focus-within { 71 | scroll-behavior: auto; 72 | } 73 | 74 | *, 75 | *::before, 76 | *::after { 77 | animation-duration: 0.01ms !important; 78 | animation-iteration-count: 1 !important; 79 | transition-duration: 0.01ms !important; 80 | scroll-behavior: auto !important; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/sass/_theme.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-primary-hs: 260, 100%; 3 | --color-secondary-hs: 310deg, 60%; 4 | --color-primary: hsl(var(--color-primary-hs), 65%); 5 | --color-on-primary: hsl(var(--color-primary-hs), 99%); 6 | --color-secondary: hsl(var(--color-secondary-hs), 48%); 7 | --color-secondary-light: hsl(var(--color-secondary-hs), 95%); 8 | --color-on-secondary: hsl(var(--color-secondary-hs), 99%); 9 | --color-light: hsl(var(--color-primary-hs), 99%); 10 | --background: hsl(var(--color-primary-hs), 95%); 11 | --text: hsl(var(--color-primary-hs), 25%); 12 | } 13 | 14 | $background-colors: "primary", "secondary", "light"; 15 | 16 | @each $color in $background-colors { 17 | .background-#{$color} { 18 | background-color: var(--color-#{$color}); 19 | 20 | @if $color != "light" { 21 | color: var(--color-on-#{$color}); 22 | 23 | a { 24 | color: inherit; 25 | } 26 | } 27 | } 28 | } 29 | 30 | .color-primary { 31 | color: var(--color-primary); 32 | } 33 | -------------------------------------------------------------------------------- /src/sass/_utilities.scss: -------------------------------------------------------------------------------- 1 | .inclusively-hidden { 2 | clip: rect(0 0 0 0); 3 | clip-path: inset(50%); 4 | height: 1px; 5 | overflow: hidden; 6 | position: absolute; 7 | white-space: nowrap; 8 | width: 1px; 9 | } 10 | 11 | .new-count { 12 | width: 3ch; 13 | height: 3ch; 14 | line-height: 3ch; 15 | display: grid; 16 | place-content: center; 17 | border-radius: 50%; 18 | font-weight: bold; 19 | font-size: 1.25rem; 20 | } 21 | 22 | .new-badge { 23 | font-size: 0.8em; 24 | padding: 0.15em 0.25em; 25 | border-radius: 0.25rem; 26 | letter-spacing: 0.03em; 27 | text-transform: uppercase; 28 | } -------------------------------------------------------------------------------- /src/sass/style.scss: -------------------------------------------------------------------------------- 1 | @use "reset"; 2 | @use "theme"; 3 | @use "layout"; 4 | @use "item-articles"; 5 | @use "card"; 6 | @use "utilities"; 7 | @use "prismtheme"; 8 | --------------------------------------------------------------------------------