From 3588ab0506f39e32746bab779f1416070cf56b4e Mon Sep 17 00:00:00 2001 From: Rui Saraiva Date: Sun, 26 Jul 2026 14:30:50 +0100 Subject: [PATCH 1/5] feat: link the author credit to a website and update the contact address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes to the about modal, in the same file: Rui's credit pointed at a Twitter handle and now points at ruisaraiva.dev, with a globe icon. Augusto keeps Twitter and Miguel keeps GitHub, so the link type is now one of three rather than a binary — hence a small lookup instead of a second ternary in the JSX. The icon is drawn from circle, line and ellipse primitives rather than path data, so the geometry is exact. Because the globe is stroked where the GitHub mark is filled, the dark-mode rule for it has to set stroke rather than fill. Both were checked in both themes. The subtitle said "follow these guys on Twitter", which only one of the three links now is, so it drops the platform. The contact address moves from hello@favycon.app, which is no longer in use, to hello@ruisaraiva.dev. --- components/favycon-info/index.module.scss | 16 +++- components/favycon-info/index.tsx | 105 ++++++++++++---------- components/svgs/svg-website.tsx | 22 +++++ 3 files changed, 95 insertions(+), 48 deletions(-) create mode 100644 components/svgs/svg-website.tsx diff --git a/components/favycon-info/index.module.scss b/components/favycon-info/index.module.scss index 280df65..5e8ed9b 100644 --- a/components/favycon-info/index.module.scss +++ b/components/favycon-info/index.module.scss @@ -290,7 +290,11 @@ margin-right: 2px; } - &.personGithub { + // Both modifiers do the same job: drop the base class's Twitter blue for the + // neutral ink. Kept as two names rather than one shared class so the markup + // still says which kind of link it is. + &.personGithub, + &.personWebsite { color: $black; p { @@ -316,7 +320,8 @@ } } -:global(.dark) .personTwitter.personGithub p { +:global(.dark) .personTwitter.personGithub p, +:global(.dark) .personTwitter.personWebsite p { color: $white; &:hover { @@ -328,6 +333,13 @@ fill: $white; } +// The GitHub mark is a filled path; the globe is stroked, so it needs the other +// property. Both are set explicitly rather than relying on currentColor, to +// match how the rule above already works. +:global(.dark) .personTwitter.personWebsite p svg { + stroke: $white; +} + .modalFooter { padding-bottom: 48px; margin-top: 12px; diff --git a/components/favycon-info/index.tsx b/components/favycon-info/index.tsx index ddbb7c2..7b65d65 100644 --- a/components/favycon-info/index.tsx +++ b/components/favycon-info/index.tsx @@ -7,6 +7,7 @@ import { Typography } from 'components/typography' import { ToolTitle } from 'components/tool-title' import { SvgTwitter } from 'components/svgs/svg-twitter' import { SvgGithub } from 'components/svgs/svg-github' +import { SvgWebsite } from 'components/svgs/svg-website' import styles from './index.module.scss' import { SvgFavycon } from 'components/svgs/svg-favycon' @@ -19,26 +20,47 @@ import miguelTeixeira from '../../public/images/people/miguel-teixeira.png' const Modal = dynamic(() => import('react-modal')) +// A lookup rather than a chain of ternaries in the JSX, because there are three +// kinds of link now. `personTwitter` is the shared base class — it predates the +// other two and carries the layout, so the modifiers only restate the colour. +const socials = { + twitter: { + Icon: SvgTwitter, + url: (handle: string) => `https://twitter.com/${handle}`, + modifier: undefined, + }, + github: { + Icon: SvgGithub, + url: (handle: string) => `https://github.com/${handle}`, + modifier: styles.personGithub, + }, + website: { + Icon: SvgWebsite, + url: (handle: string) => `https://${handle}`, + modifier: styles.personWebsite, + }, +} + const people = [ { - screenName: 'aboutaugusto', + handle: 'aboutaugusto', name: 'Augusto Lopes', role: 'Product Designer', - social: 'twitter', + social: 'twitter' as const, photo: augustoLopes, }, { - screenName: 'ruisaraiva19', + handle: 'ruisaraiva.dev', name: 'Rui Saraiva', role: 'Full-Stack Developer', - social: 'twitter', + social: 'website' as const, photo: ruiSaraiva, }, { - screenName: 'miguellteixeira', + handle: 'miguellteixeira', name: 'Miguel Teixeira', role: 'Full-Stack Developer', - social: 'github', + social: 'github' as const, photo: miguelTeixeira, }, ] @@ -138,56 +160,47 @@ const FavyconInfo = ({ Behind the curtains - For new updates be sure to follow these guys on Twitter. + For new updates be sure to follow these guys.
- {people.map((person) => ( -
-
- {person.name} -
-
- - {person.name} - - - {person.role} - - {person.social === 'twitter' ? ( + {people.map((person) => { + const { Icon, url, modifier } = socials[person.social] + + return ( +
+
+ {person.name} +
+
+ + {person.name} + + + {person.role} + - {person.screenName} + {person.handle} - ) : ( - - - {person.screenName} - - - )} +
-
- ))} + ) + })}

@@ -202,8 +215,8 @@ const FavyconInfo = ({ GitHub . Have feedback? Send it to{' '} - - hello@favycon.app + + hello@ruisaraiva.dev
diff --git a/components/svgs/svg-website.tsx b/components/svgs/svg-website.tsx new file mode 100644 index 0000000..b0b8301 --- /dev/null +++ b/components/svgs/svg-website.tsx @@ -0,0 +1,22 @@ +// Drawn from primitives rather than path data so the geometry is exact: a +// circle, the equator and one meridian. Stroked rather than filled, which is +// why the dark-mode rule for .personWebsite sets `stroke` where .personGithub +// sets `fill`. +const SvgWebsite: React.FC> = (props) => ( + + + + + +) + +export { SvgWebsite } From 78c9508195f6f25aac36f5d5c88b97b2e32dc4b4 Mon Sep 17 00:00:00 2001 From: Rui Saraiva Date: Sun, 26 Jul 2026 14:31:02 +0100 Subject: [PATCH 2/5] feat(seo): declare the author and add llms.txt The about modal is lazily mounted, so the credits and contact address are not in the server-rendered HTML and no crawler ever sees them. Authorship therefore goes in the head, where it is always present: a meta author, a rel=author link to ruisaraiva.dev, and SoftwareApplication JSON-LD. The structured data names Rui as author and Augusto and Miguel as contributors, because the modal credits three people and the markup should not tell a machine a tidier story than the page tells a reader. Also adds public/llms.txt: accepted input formats, the 310px and 512px minimums, and exactly what the zip contains, including that favicon.svg only appears for SVG input and manifest.json only with the PWA option. It is written from utils/favicon.ts and the route handler rather than from memory. Note it is a static file, so it is the one thing here that can drift from the sizes it documents. Two hardcoded favycon.vercel.app strings become the SITE_URL constant the rest of the file now uses. --- components/seo/index.tsx | 103 ++++++++++++++++++++++++++------------- public/llms.txt | 48 ++++++++++++++++++ 2 files changed, 117 insertions(+), 34 deletions(-) create mode 100644 public/llms.txt diff --git a/components/seo/index.tsx b/components/seo/index.tsx index 8b33f8b..11b76f0 100644 --- a/components/seo/index.tsx +++ b/components/seo/index.tsx @@ -1,43 +1,78 @@ import Head from 'next/head' +const SITE_URL = 'https://favycon.vercel.app' + +const AUTHOR = { name: 'Rui Saraiva', url: 'https://ruisaraiva.dev' } + +// The tool credits three people in its About modal, so the structured data says +// the same thing rather than a tidier untruth: Rui as author, the other two as +// contributors, linked where the modal links them. +const CONTRIBUTORS = [ + { name: 'Augusto Lopes', url: 'https://twitter.com/aboutaugusto' }, + { name: 'Miguel Teixeira', url: 'https://github.com/miguellteixeira' }, +] + type SEOProps = { title: string description: string } -const SEO = ({ title, description }: SEOProps) => ( - - {title} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -) +const SEO = ({ title, description }: SEOProps) => { + const structuredData = { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: 'Favycon', + description, + url: SITE_URL, + applicationCategory: 'DeveloperApplication', + operatingSystem: 'Any', + browserRequirements: 'Requires JavaScript', + image: `${SITE_URL}/share.png`, + license: 'https://opensource.org/licenses/MIT', + // It is genuinely free, and Google wants the price stated to believe it. + offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' }, + author: { '@type': 'Person', ...AUTHOR }, + contributor: CONTRIBUTORS.map((person) => ({ '@type': 'Person', ...person })), + codeRepository: 'https://github.com/ruisaraiva19/favycon', + } + + return ( + + {title} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +