├── .all-contributorsrc ├── .changeset └── config.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── svead.svg └── workflows │ ├── e2e-ci.yml │ └── unit-test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── apps └── web │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── README.md │ ├── mdsvex.config.js │ ├── package.json │ ├── playwright.config.ts │ ├── postcss.config.cjs │ ├── src │ ├── app.d.ts │ ├── app.html │ ├── app.postcss │ ├── index.test.ts │ ├── lib │ │ ├── components │ │ │ ├── details.svelte │ │ │ └── index.ts │ │ ├── copy │ │ │ ├── blog-posting-copy.md │ │ │ ├── breadcrumbs-copy.md │ │ │ ├── index-copy.md │ │ │ ├── multiple-ld-json-sections-copy.md │ │ │ ├── news-article-copy.md │ │ │ └── web-page-copy.md │ │ ├── icons │ │ │ ├── github.svelte │ │ │ ├── index.ts │ │ │ ├── twitter.svelte │ │ │ └── youtube.svelte │ │ └── index.ts │ ├── prism.css │ └── routes │ │ ├── +layout.svelte │ │ ├── +page.svelte │ │ ├── +page.ts │ │ ├── article │ │ └── +page.svelte │ │ ├── blog-posting │ │ ├── +page.svelte │ │ └── +page.ts │ │ ├── breadcrumbs │ │ ├── +page.svelte │ │ └── +page.ts │ │ ├── multiple-ld-json-sections │ │ ├── +page.svelte │ │ └── +page.ts │ │ ├── news-article │ │ ├── +page.svelte │ │ └── +page.ts │ │ └── web-page │ │ ├── +page.svelte │ │ └── +page.ts │ ├── static │ ├── favicon.png │ └── spencee.png │ ├── svelte.config.js │ ├── tailwind.config.cjs │ ├── tests │ └── index.test.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.ts ├── package.json ├── packages └── svead │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── playwright.config.ts │ ├── src │ ├── app.d.ts │ ├── app.html │ ├── index.test.ts │ ├── lib │ │ ├── components │ │ │ ├── head.svelte │ │ │ ├── head.test.ts │ │ │ ├── schema-org.svelte │ │ │ └── schema-org.test.ts │ │ ├── index.ts │ │ └── types │ │ │ ├── index.ts │ │ │ ├── schema-org.ts │ │ │ └── seo-config.ts │ └── routes │ │ └── +page.svelte │ ├── static │ └── favicon.png │ ├── svelte.config.js │ ├── tests │ └── test.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── renovate.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "svead", 3 | "projectOwner": "spences10", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "none", 12 | "contributors": [ 13 | { 14 | "login": "spences10", 15 | "name": "Scott Spence", 16 | "avatar_url": "https://avatars.githubusercontent.com/u/234708?v=4", 17 | "profile": "https://scottspence.com/", 18 | "contributions": [ 19 | "code", 20 | "doc", 21 | "example", 22 | "maintenance", 23 | "test" 24 | ] 25 | } 26 | ], 27 | "contributorsPerLine": 7, 28 | "linkToUsage": true 29 | } 30 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | 15 | Steps to reproduce the behavior: 16 | 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | **Desktop (please complete the following information):** 31 | 32 | - OS: [e.g. iOS] 33 | - Browser [e.g. chrome, safari] 34 | - Version [e.g. 22] 35 | 36 | **Smartphone (please complete the following information):** 37 | 38 | - Device: [e.g. iPhone6] 39 | - OS: [e.g. iOS8.1] 40 | - Browser [e.g. stock browser, safari] 41 | - Version [e.g. 22] 42 | 43 | **Additional context** 44 | 45 | Add any other context about the problem here. 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | 11 | A clear and concise description of what the problem is. Ex. I'm always 12 | frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | 20 | A clear and concise description of any alternative solutions or 21 | features you've considered. 22 | 23 | **Additional context** 24 | 25 | Add any other context or screenshots about the feature request here. 26 | -------------------------------------------------------------------------------- /.github/workflows/e2e-ci.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests: E2E' 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | tests_e2e: 11 | name: Run end-to-end tests 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 18.x 18 | - uses: actions/cache@v4 19 | with: 20 | path: ~/.pnpm-store 21 | # prettier-ignore 22 | key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} 23 | restore-keys: ${{ runner.os }}-pnpm- 24 | - uses: pnpm/action-setup@v4.1.0 25 | with: 26 | version: '^8.0.0' 27 | - name: Install dependencies 28 | run: pnpm i 29 | - name: Build svead package 30 | run: pnpm run build 31 | working-directory: packages/svead 32 | - name: Install playwright browsers 33 | run: npx playwright install --with-deps 34 | working-directory: apps/web 35 | - name: Test 36 | run: pnpm run test 37 | working-directory: apps/web 38 | env: 39 | PUBLIC_FATHOM_ID: ${{ secrets.PUBLIC_FATHOM_ID }} 40 | PUBLIC_FATHOM_URL: ${{ secrets.PUBLIC_FATHOM_URL }} 41 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests: Unit' 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | types: [opened, synchronize] 9 | 10 | jobs: 11 | unit_tests: 12 | name: Run unit tests for Package 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 18.x 20 | - uses: actions/cache@v4 21 | with: 22 | path: ~/.pnpm-store 23 | # prettier-ignore 24 | key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} 25 | restore-keys: ${{ runner.os }}-pnpm- 26 | - uses: pnpm/action-setup@v4.1.0 27 | with: 28 | version: '^8.0.0' 29 | - name: Install dependencies 30 | run: pnpm recursive install 31 | - name: Run unit tests 32 | run: pnpm run test:ci 33 | working-directory: packages/svead 34 | env: 35 | PUBLIC_FATHOM_ID: ${{ secrets.PUBLIC_FATHOM_ID }} 36 | PUBLIC_FATHOM_URL: ${{ secrets.PUBLIC_FATHOM_URL }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /dist 5 | /.svelte-kit 6 | /package 7 | .env 8 | .env.* 9 | !.env.example 10 | .vercel 11 | vite.config.js.timestamp-* 12 | vite.config.ts.timestamp-* 13 | coverage/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore files for PNPM, NPM and YARN 2 | pnpm-lock.yaml 3 | package-lock.json 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 70, 6 | "arrowParens": "avoid", 7 | "proseWrap": "always", 8 | "plugins": ["prettier-plugin-svelte"], 9 | "overrides": [ 10 | { "files": "*.svelte", "options": { "parser": "svelte" } } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "streetsidesoftware.code-spell-checker", 4 | "svelte.svelte-vscode" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.enableSmartCommit": true, 3 | "git.postCommitCommand": "sync", 4 | "cSpell.words": [ 5 | "Ahrefs", 6 | "daisyui", 7 | "Kazuma", 8 | "mdsvex", 9 | "noopener", 10 | "noreferrer", 11 | "oekazuma", 12 | "pnpm", 13 | "spencee", 14 | "svead", 15 | "sveltejs", 16 | "vite" 17 | ], 18 | "css.validate": false 19 | } 20 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Svelte Code of Conduct 2 | 3 | This project is a Svelte Community project and therefore uses the same 4 | [Code of Conduct](https://github.com/sveltejs/community/blob/main/CODE_OF_CONDUCT.md) 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Scott Spence 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Svead 🍺 - Svelte Head Component 2 | 3 | 4 | 5 | [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) 6 | 7 | 8 | 9 | [![MadeWithSvelte.com shield](https://madewithsvelte.com/storage/repo-shields/4099-shield.svg)](https://madewithsvelte.com/p/svead/shield-link) 10 | 11 | [![Tests: E2E](https://github.com/spences10/svead/actions/workflows/e2e-ci.yml/badge.svg)](https://github.com/spences10/svead/actions/workflows/e2e-ci.yml) 12 | 13 | [![Tests: Unit](https://github.com/spences10/svead/actions/workflows/unit-test.yml/badge.svg)](https://github.com/spences10/svead/actions/workflows/unit-test.yml) 14 | 15 | Svead, a component that allows you to set head meta information, 16 | canonical, title, Twitter and Facebook Open Graph tags. 17 | 18 | Also supports JSON-LD for SEO with the `SchemaOrg` component. 19 | 20 | ![svead](.github/svead.svg) 21 | 22 | ## Name 23 | 24 | The name was meant to be Svelte + Head, but I like Puru's suggestion 25 | of Svelte + Mead 26 | 27 | ## v0.0.4 vs v1 28 | 29 | v1 is currently available via `pnpm i -D svead@next` and will be that 30 | way until Svelte 5 comes out of RC phase. 31 | 32 | v1 has changed compared to v0.0.4. The main change is that the there's 33 | one config object with `SeoConfig`. 34 | 35 | Separated out the `SchemaOrg` component from the `Head` component 36 | which can be optionally used to add structured data to your web pages. 37 | 38 | ```svelte 39 | 56 | 57 | 58 | 59 | 60 |

Welcome to My Site

61 |

This is a simple web page example.

62 | ``` 63 | 64 | ## Props 65 | 66 | It takes the following props: 67 | 68 | ### `SeoConfig` Props 69 | 70 | | Property | Type | Description | Required | 71 | | :------------------ | :----------------- | :----------------------------------------------------------- | :------- | 72 | | `title` | `string` | The title of the web page. | Yes | 73 | | `description` | `string` | A description of the web page. | Yes | 74 | | `url` | `string` | The URL of the web page. | Yes | 75 | | `website` | `string` | The website the web page belongs to. | No | 76 | | `language` | `string` \| `'en'` | The language of the web page. Defaults to 'en'. | No | 77 | | `open_graph_image` | `string` | The URL of an image to use for Open Graph meta tags. | No | 78 | | `payment_pointer` | `string` | A payment pointer for Web Monetization. | No | 79 | | `author_name` | `string` | The name of the author. | No | 80 | | `site_name` | `string` | The name of the site for og:site_name. | No | 81 | | `twitter_handle` | `string` | The Twitter handle of the content creator or site. | No | 82 | | `twitter_card_type` | `string` | The type of Twitter card. Defaults to 'summary_large_image'. | No | 83 | 84 | ## SchemaOrg Component 85 | 86 | The SchemaOrg component allows you to add structured data to your web 87 | pages using JSON-LD format. This helps search engines better 88 | understand your content and can potentially improve your site's 89 | appearance in search results. 90 | 91 | ### Usage 92 | 93 | ```svelte 94 | 108 | 109 | 110 | ``` 111 | 112 | ### `SchemaOrgProps` Props 113 | 114 | | Property | Type | Description | Required | 115 | | :------- | :-------------- | :---------------------------------------------------------- | :------- | 116 | | `schema` | `SchemaOrgType` | The structured data object following schema.org vocabulary. | Yes | 117 | 118 | ### `SchemaOrgType` 119 | 120 | `SchemaOrgType` is a union type that includes: 121 | 122 | - `Thing`: Represents the most generic type of item in schema.org. 123 | - `WithContext`: A Thing with an added `@context` property. 124 | 125 | You can use any valid schema.org type as defined in the 126 | [schema.org documentation](https://schema.org). 127 | 128 | ### Additional Notes: 129 | 130 | - The `@context` property is automatically added by the component if 131 | not provided. 132 | - You can include multiple schema types by nesting them within the 133 | main schema object. 134 | - Always validate your structured data using tools like 135 | [Google's Rich Results Test](https://search.google.com/test/rich-results) 136 | to ensure it's correctly formatted. 137 | 138 | ### Example with Multiple Schema Types 139 | 140 | ```svelte 141 | 159 | 160 | 161 | ``` 162 | 163 | ## Packaging for NPM 164 | 165 | Scott, this is here for you to remember how to do this 🙃 166 | 167 | Although I detailed this in 168 | [Making npm Packages with SvelteKit](https://scottspence.com/posts/making-npm-packages-with-sveltekit) 169 | I think it's best to put it here as I always come to the README and 170 | the instructions are never there! 😅 171 | 172 | **Publish the project to NPM** 173 | 174 | ```bash 175 | # change to package directory 176 | cd packages/svead 177 | # authenticate with npm 178 | npm login 179 | # bump version with npm 180 | npm version 0.0.8 181 | # package with sveltekit 182 | pnpm run package 183 | # publish 184 | npm publish 185 | # push tags to github 186 | git push --tags 187 | ``` 188 | 189 | **Publish @next package** 190 | 191 | Same procedure except use the `--tag` flag: 192 | 193 | ```bash 194 | # change to package directory 195 | cd packages/svead 196 | # authenticate with npm 197 | npm login 198 | # bump version with npm 199 | npm version 0.0.13 200 | # package with sveltekit 201 | pnpm run package 202 | # publish with tag 203 | npm publish --tag next 204 | # push tags to github 205 | git push --tags 206 | ``` 207 | 208 | **Move @next package to latest** 209 | 210 | ```bash 211 | # authenticate with npm 212 | npm login 213 | # move @next to latest 214 | npm dist-tag add svead@0.0.13 latest 215 | ``` 216 | 217 | ## pnpm workspaces 218 | 219 | To add the `svead` package to the `web` workspace: 220 | 221 | ```bash 222 | pnpm add -D svead --filter web 223 | ``` 224 | 225 | ## Contributors ✨ 226 | 227 | Thanks goes to these wonderful people 228 | ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 246 | 247 | 248 |
Scott Spence
Scott Spence

💻 📖 💡 🚧 ⚠️
242 | 243 | Add your contributions 244 | 245 |
249 | 250 | 251 | 252 | 253 | 254 | 255 | This project follows the 256 | [all-contributors](https://github.com/all-contributors/all-contributors) 257 | specification. Contributions of any kind welcome! 258 | -------------------------------------------------------------------------------- /apps/web/.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type { import("eslint").Linter.Config } */ 2 | module.exports = { 3 | root: true, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:svelte/recommended', 8 | 'prettier', 9 | ], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['@typescript-eslint'], 12 | parserOptions: { 13 | sourceType: 'module', 14 | ecmaVersion: 2020, 15 | extraFileExtensions: ['.svelte'], 16 | }, 17 | env: { 18 | browser: true, 19 | es2017: true, 20 | node: true, 21 | }, 22 | overrides: [ 23 | { 24 | files: ['*.svelte'], 25 | parser: 'svelte-eslint-parser', 26 | parserOptions: { 27 | parser: '@typescript-eslint/parser', 28 | }, 29 | }, 30 | ], 31 | }; 32 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | .vercel 12 | screenshots -------------------------------------------------------------------------------- /apps/web/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /apps/web/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore files for PNPM, NPM and YARN 2 | pnpm-lock.yaml 3 | package-lock.json 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /apps/web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 70, 6 | "arrowParens": "avoid", 7 | "proseWrap": "always", 8 | "plugins": [ 9 | "prettier-plugin-svelte", 10 | "prettier-plugin-tailwindcss" 11 | ], 12 | "overrides": [ 13 | { 14 | "files": "*.svelte", 15 | "options": { 16 | "parser": "svelte" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | # Svead 🍺 - Svelte Head Component 2 | 3 | [docs](../../README.md) -------------------------------------------------------------------------------- /apps/web/mdsvex.config.js: -------------------------------------------------------------------------------- 1 | import { defineMDSveXConfig as defineConfig } from 'mdsvex'; 2 | import autolinkHeadings from 'rehype-autolink-headings'; 3 | import slugPlugin from 'rehype-slug'; 4 | 5 | const config = defineConfig({ 6 | extensions: ['.svelte.md', '.md', '.svx'], 7 | 8 | smartypants: { 9 | dashes: 'oldschool', 10 | }, 11 | 12 | remarkPlugins: [], 13 | rehypePlugins: [ 14 | slugPlugin, 15 | [ 16 | autolinkHeadings, 17 | { 18 | behavior: 'wrap', 19 | }, 20 | ], 21 | ], 22 | }); 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "pnpm run build:packages && vite build", 8 | "preview": "vite preview", 9 | "test": "npm run test:integration && npm run test:unit", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 12 | "lint": "prettier --check . && eslint .", 13 | "format": "prettier --write .", 14 | "test:integration": "playwright test", 15 | "test:unit": "vitest", 16 | "coverage": "vitest run --coverage", 17 | "build:packages": "pnpm -r --filter=\"../../packages/*\" run build" 18 | }, 19 | "devDependencies": { 20 | "@playwright/test": "^1.50.1", 21 | "@sveltejs/adapter-auto": "^4.0.0", 22 | "@sveltejs/kit": "^2.17.1", 23 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 24 | "@tailwindcss/typography": "^0.5.16", 25 | "@testing-library/svelte": "^5.2.6", 26 | "@types/eslint": "9.6.1", 27 | "@typescript-eslint/eslint-plugin": "^8.23.0", 28 | "@typescript-eslint/parser": "^8.23.0", 29 | "@vitest/coverage-v8": "^3.0.5", 30 | "autoprefixer": "^10.4.20", 31 | "daisyui": "^4.12.23", 32 | "eslint": "^9.19.0", 33 | "eslint-config-prettier": "^10.0.1", 34 | "eslint-plugin-svelte": "^2.46.1", 35 | "fathom-client": "^3.7.2", 36 | "jsdom": "^26.0.0", 37 | "mdsvex": "^0.12.3", 38 | "postcss": "^8.5.1", 39 | "postcss-load-config": "^6.0.1", 40 | "prettier": "^3.4.2", 41 | "prettier-plugin-svelte": "^3.3.3", 42 | "prettier-plugin-tailwindcss": "^0.6.11", 43 | "rehype-autolink-headings": "^7.1.0", 44 | "rehype-slug": "^6.0.0", 45 | "svead": "workspace:*", 46 | "svelte": "5.26.2", 47 | "svelte-check": "^4.1.4", 48 | "tailwindcss": "^3.4.17", 49 | "tslib": "^2.8.1", 50 | "typescript": "^5.7.3", 51 | "vite": "^6.0.11", 52 | "vitest": "^3.0.5" 53 | }, 54 | "type": "module" 55 | } 56 | -------------------------------------------------------------------------------- /apps/web/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | 3 | const config: PlaywrightTestConfig = { 4 | webServer: { 5 | command: 'pnpm run build && pnpm run preview', 6 | port: 4173, 7 | }, 8 | testDir: 'tests', 9 | testMatch: /(.+\.)?(test|spec)\.[jt]s/, 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /apps/web/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss'); 2 | const autoprefixer = require('autoprefixer'); 3 | 4 | const config = { 5 | plugins: [ 6 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind, 7 | tailwindcss(), 8 | //But others, like autoprefixer, need to run after, 9 | autoprefixer, 10 | ], 11 | }; 12 | 13 | module.exports = config; 14 | -------------------------------------------------------------------------------- /apps/web/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /apps/web/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | %sveltekit.head% 11 | 12 | 13 |
%sveltekit.body%
14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web/src/app.postcss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | html { 4 | scroll-behavior: smooth; 5 | /* margin-left: calc(100vw - 100%); */ 6 | word-break: break-word; 7 | } 8 | 9 | /* Scrollbar styles */ 10 | 11 | /* Firefox */ 12 | * { 13 | scrollbar-width: thin; 14 | scrollbar-color: oklch(var(--s)) oklch(var(--p)); 15 | } 16 | 17 | /* Chrome, Edge, and Safari */ 18 | *::-webkit-scrollbar { 19 | width: 15px; 20 | } 21 | 22 | *::-webkit-scrollbar-track { 23 | background: oklch(var(--p)); 24 | border-radius: 5px; 25 | } 26 | 27 | *::-webkit-scrollbar-thumb { 28 | background-color: oklch(var(--s)); 29 | border-radius: 14px; 30 | border: 3px solid oklch(var(--p)); 31 | } 32 | 33 | *::-webkit-scrollbar-thumb:hover { 34 | background-color: oklch(var(--a)); 35 | } 36 | 37 | @tailwind components; 38 | @tailwind utilities; 39 | -------------------------------------------------------------------------------- /apps/web/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/details.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 | 27 | {#if isOpen} 28 |
33 | {@render children?.()} 34 |
35 | {/if} 36 |
37 | -------------------------------------------------------------------------------- /apps/web/src/lib/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Details } from './details.svelte'; 2 | -------------------------------------------------------------------------------- /apps/web/src/lib/copy/blog-posting-copy.md: -------------------------------------------------------------------------------- 1 | Here's the code for this page: 2 | 3 | ```svelte 4 | 102 | 103 | 104 | 105 | 106 |
107 |

{seo_config.title}

108 |

{seo_config.description}

109 | 110 | 111 |
112 | ``` 113 | -------------------------------------------------------------------------------- /apps/web/src/lib/copy/breadcrumbs-copy.md: -------------------------------------------------------------------------------- 1 | Here's the code for this page: 2 | 3 | ```svelte 4 | 111 | 112 | 113 | 114 | 115 |

{page_title}

116 |

{page_description}

117 | 118 | 119 | ``` 120 | -------------------------------------------------------------------------------- /apps/web/src/lib/copy/index-copy.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Welcome to Svead 🍺 6 | 7 | The Svelte Head and Schema.org Component. 8 | 9 | Svead is a dynamic component that enhances your SEO by allowing you to 10 | set head meta information for canonical, title, Twitter, Facebook, 11 | Open Graph tags, and JSON-LD structured data. 12 | 13 | Visit [GitHub](https://github.com/spences10/svead) to contribute to 14 | this project. 15 | 16 | ## Components 17 | 18 | Svead provides two main components: 19 | 20 | 1. `Head`: For setting meta tags and other head information. 21 | 2. `SchemaOrg`: For adding structured data using JSON-LD. 22 | 23 | ## Example Routes 24 | 25 | Explore how Svead works with different content types: 26 | 27 | - [Breadcrumbs](/breadcrumbs) 28 | - [Article](/article) 29 | - [Blog Posting](/blog-posting) 30 | - [News Article](/news-article) 31 | - [Web Page](/web-page) 32 | - [Multiple JSON-LD Sections](/multiple-ld-json-sections) 33 | 34 | ## Head Component 35 | 36 | ### Usage 37 | 38 | ```svelte 39 | 48 | 49 | 50 | ``` 51 | 52 | ### `SeoConfig` Props 53 | 54 |
55 | 56 | | Property | Type | Description | Required | 57 | | :------------------ | :----------------- | :----------------------------------------------------------- | :------- | 58 | | `title` | `string` | The title of the web page. | Yes | 59 | | `description` | `string` | A description of the web page. | Yes | 60 | | `url` | `string` | The URL of the web page. | Yes | 61 | | `website` | `string` | The website the web page belongs to. | No | 62 | | `language` | `string` \| `'en'` | The language of the web page. Defaults to 'en'. | No | 63 | | `open_graph_image` | `string` | The URL of an image to use for Open Graph meta tags. | No | 64 | | `payment_pointer` | `string` | A payment pointer for Web Monetization. | No | 65 | | `author_name` | `string` | The name of the author. | No | 66 | | `site_name` | `string` | The name of the site for og:site_name. | No | 67 | | `twitter_handle` | `string` | The Twitter handle of the content creator or site. | No | 68 | | `twitter_card_type` | `string` | The type of Twitter card. Defaults to 'summary_large_image'. | No | 69 | 70 |
71 | 72 | ## SchemaOrg Component 73 | 74 | ### Usage 75 | 76 | ```svelte 77 | 91 | 92 | 93 | ``` 94 | 95 | ### `SchemaOrgProps` Props 96 | 97 |
98 | 99 | | Property | Type | Description | Required | 100 | | :------- | :-------------- | :---------------------------------------------------------- | :------- | 101 | | `schema` | `SchemaOrgType` | The structured data object following schema.org vocabulary. | Yes | 102 | 103 |
104 | 105 | ### `SchemaOrgType` 106 | 107 | `SchemaOrgType` is extended from 108 | [schema-dts](https://github.com/google/schema-dts) and is a union type 109 | that includes: 110 | 111 | - `Thing`: Represents the most generic type of item in schema.org. 112 | - `WithContext`: A Thing with an added `@context` property. 113 | 114 | You can use any valid schema.org type as defined in the 115 | [schema.org documentation](https://schema.org). 116 | 117 | ### Additional Notes: 118 | 119 | - The `@context` property is automatically added by the component if 120 | not provided. 121 | - You can include multiple schema types by nesting them within the 122 | main schema object. 123 | - Always validate your structured data using tools like 124 | [Google's Rich Results Test](https://search.google.com/test/rich-results) 125 | to ensure it's correctly formatted. 126 | 127 | ## Example with Both Components 128 | 129 | ```svelte 130 | 158 | 159 | 160 | 161 | 162 |
163 |

{seo_config.title}

164 |

{seo_config.description}

165 | 166 |
167 | ``` 168 | 169 | This example demonstrates how to use both the `Head` and `SchemaOrg` 170 | components together in a Svelte page, providing both meta tags and 171 | structured data for improved SEO. 172 | 173 | For more information and full documentation, visit the 174 | [Svead GitHub repository](https://github.com/spences10/svead). 175 | -------------------------------------------------------------------------------- /apps/web/src/lib/copy/multiple-ld-json-sections-copy.md: -------------------------------------------------------------------------------- 1 | Here's the code for this page: 2 | 3 | ```svelte 4 | 123 | 124 | 125 | 126 | 127 |
128 | 135 | 136 |

{seo_config.title}

137 |

{seo_config.description}

138 | 139 | 140 | 141 | 142 |
143 |

Introduction to Structured Data

144 |

145 | Structured data helps search engines understand the content of 146 | your web pages... 147 |

148 |
149 | 150 |
151 |

Implementing JSON-LD in SvelteKit

152 |

Here's how you can add JSON-LD to your SvelteKit project...

153 |
154 | 155 | 156 | 157 |
158 |

Author: {seo_config.author_name}

159 |

Published: {new Date().toLocaleDateString()}

160 |
161 |
162 | ``` 163 | -------------------------------------------------------------------------------- /apps/web/src/lib/copy/news-article-copy.md: -------------------------------------------------------------------------------- 1 | Here's the code for this page: 2 | 3 | ```svelte 4 | 117 | 118 | 119 | 120 | 121 |
122 |

{seo_config.title}

123 |

{seo_config.description}

124 | 125 | 126 |
127 | ``` 128 | -------------------------------------------------------------------------------- /apps/web/src/lib/copy/web-page-copy.md: -------------------------------------------------------------------------------- 1 | Here's the code for this page: 2 | 3 | ```svelte 4 | 86 | 87 | 88 | 89 | 90 |
91 |

{seo_config.title}

92 |

{seo_config.description}

93 | 94 | 95 |
96 | ``` 97 | -------------------------------------------------------------------------------- /apps/web/src/lib/icons/github.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /apps/web/src/lib/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as GitHub } from './github.svelte'; 2 | export { default as Twitter } from './twitter.svelte'; 3 | export { default as YouTube } from './youtube.svelte'; 4 | -------------------------------------------------------------------------------- /apps/web/src/lib/icons/twitter.svelte: -------------------------------------------------------------------------------- 1 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /apps/web/src/lib/icons/youtube.svelte: -------------------------------------------------------------------------------- 1 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /apps/web/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './icons'; 2 | export * from './components'; 3 | -------------------------------------------------------------------------------- /apps/web/src/prism.css: -------------------------------------------------------------------------------- 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[class*='language-'], 10 | pre[class*='language-'] { 11 | color: #d6deeb; 12 | font-family: 'Victor Mono', Consolas, Monaco, 'Andale Mono', 13 | 'Ubuntu Mono', monospace; 14 | text-align: left; 15 | white-space: pre; 16 | word-spacing: normal; 17 | word-break: normal; 18 | word-wrap: normal; 19 | 20 | -moz-tab-size: 4; 21 | -o-tab-size: 4; 22 | tab-size: 4; 23 | 24 | -webkit-hyphens: none; 25 | -moz-hyphens: none; 26 | -ms-hyphens: none; 27 | hyphens: none; 28 | } 29 | 30 | pre[class*='language-']::-moz-selection, 31 | pre[class*='language-'] ::-moz-selection, 32 | code[class*='language-']::-moz-selection, 33 | code[class*='language-'] ::-moz-selection { 34 | text-shadow: none; 35 | background: rgba(29, 59, 83, 0.99); 36 | } 37 | 38 | pre[class*='language-']::selection, 39 | pre[class*='language-'] ::selection, 40 | code[class*='language-']::selection, 41 | code[class*='language-'] ::selection { 42 | text-shadow: none; 43 | background: rgba(29, 59, 83, 0.99); 44 | } 45 | 46 | @media print { 47 | code[class*='language-'], 48 | pre[class*='language-'] { 49 | text-shadow: none; 50 | } 51 | } 52 | 53 | /* Code blocks */ 54 | pre[class*='language-'] { 55 | /* padding: 1em; */ 56 | /* margin: 0.5em 0; */ 57 | overflow: auto; 58 | } 59 | 60 | :not(pre) > code[class*='language-'], 61 | pre[class*='language-'] { 62 | color: #d6deeb; 63 | background: #19212e; 64 | } 65 | 66 | :not(pre) > code[class*='language-'] { 67 | /* padding: 0.1em; */ 68 | border-radius: 0.3em; 69 | white-space: normal; 70 | } 71 | 72 | .token.comment, 73 | .token.prolog, 74 | .token.cdata { 75 | color: rgb(99, 119, 119); 76 | } 77 | 78 | .token.punctuation { 79 | color: rgb(199, 146, 234); 80 | } 81 | 82 | .namespace { 83 | color: rgb(178, 204, 214); 84 | } 85 | 86 | .token.deleted { 87 | color: rgba(239, 83, 80, 0.56); 88 | font-style: italic; 89 | } 90 | 91 | .token.symbol, 92 | .token.property { 93 | color: rgb(128, 203, 196); 94 | } 95 | 96 | .token.tag, 97 | .token.operator, 98 | .token.keyword { 99 | color: rgb(127, 219, 202); 100 | } 101 | 102 | .token.boolean { 103 | color: rgb(255, 88, 116); 104 | } 105 | 106 | .token.number { 107 | color: rgb(247, 140, 108); 108 | } 109 | 110 | .token.constant, 111 | .token.function, 112 | .token.builtin, 113 | .token.char { 114 | color: rgb(130, 170, 255); 115 | } 116 | 117 | .token.selector, 118 | .token.doctype { 119 | color: rgb(199, 146, 234); 120 | font-style: italic; 121 | } 122 | 123 | .token.attr-name, 124 | .token.inserted { 125 | color: rgb(173, 219, 103); 126 | font-style: italic; 127 | } 128 | 129 | .token.string, 130 | .token.url, 131 | .token.entity, 132 | .language-css .token.string, 133 | .style .token.string { 134 | color: rgb(173, 219, 103); 135 | } 136 | 137 | .token.class-name, 138 | .token.atrule, 139 | .token.attr-value { 140 | color: rgb(255, 203, 139); 141 | } 142 | 143 | .token.regex, 144 | .token.important, 145 | .token.variable { 146 | color: rgb(214, 222, 235); 147 | } 148 | 149 | .token.important, 150 | .token.bold { 151 | font-weight: bold; 152 | } 153 | 154 | .token.italic { 155 | font-style: italic; 156 | } 157 | -------------------------------------------------------------------------------- /apps/web/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 | 27 | 44 | 45 |
46 | 47 |
50 | 51 |
52 | 53 | 111 | -------------------------------------------------------------------------------- /apps/web/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /apps/web/src/routes/+page.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | export const load = async () => { 4 | const slug = 'index-copy'; 5 | try { 6 | const Copy = await import(`../lib/copy/${slug}.md`); 7 | return { 8 | Copy: Copy.default, 9 | }; 10 | } catch (e) { 11 | throw error(404, 'Uh oh!'); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/src/routes/article/+page.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 | 36 | 37 |

Article Example

38 | -------------------------------------------------------------------------------- /apps/web/src/routes/blog-posting/+page.svelte: -------------------------------------------------------------------------------- 1 | 99 | 100 | 101 | 102 | 103 |
104 |

{seo_config.title}

105 |

{seo_config.description}

106 | 107 | 108 |
109 | -------------------------------------------------------------------------------- /apps/web/src/routes/blog-posting/+page.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | export const load = async () => { 4 | const slug = 'blog-posting-copy'; 5 | try { 6 | const Copy = await import(`../../lib/copy/${slug}.md`); 7 | return { 8 | Copy: Copy.default, 9 | }; 10 | } catch (e) { 11 | throw error(404, 'Uh oh!'); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/src/routes/breadcrumbs/+page.svelte: -------------------------------------------------------------------------------- 1 | 108 | 109 | 110 | 111 | 112 |

{page_title}

113 |

{page_description}

114 | 115 | 116 | -------------------------------------------------------------------------------- /apps/web/src/routes/breadcrumbs/+page.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | export const load = async () => { 4 | const slug = 'breadcrumbs-copy'; 5 | try { 6 | const Copy = await import(`../../lib/copy/${slug}.md`); 7 | return { 8 | Copy: Copy.default, 9 | }; 10 | } catch (e) { 11 | throw error(404, 'Uh oh!'); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/src/routes/multiple-ld-json-sections/+page.svelte: -------------------------------------------------------------------------------- 1 | 120 | 121 | 122 | 123 | 124 |
125 | 132 | 133 |

{seo_config.title}

134 |

{seo_config.description}

135 | 136 | 137 | 138 | 139 |
140 |

Introduction to Structured Data

141 |

142 | Structured data helps search engines understand the content of 143 | your web pages... 144 |

145 |
146 | 147 |
148 |

Implementing JSON-LD in SvelteKit

149 |

Here's how you can add JSON-LD to your SvelteKit project...

150 |
151 | 152 | 153 | 154 |
155 |

Author: {seo_config.author_name}

156 |

Published: {new Date().toLocaleDateString()}

157 |
158 |
159 | -------------------------------------------------------------------------------- /apps/web/src/routes/multiple-ld-json-sections/+page.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | export const load = async () => { 4 | const slug = 'multiple-ld-json-sections-copy'; 5 | try { 6 | const Copy = await import(`../../lib/copy/${slug}.md`); 7 | return { 8 | Copy: Copy.default, 9 | }; 10 | } catch (e) { 11 | throw error(404, 'Uh oh!'); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/src/routes/news-article/+page.svelte: -------------------------------------------------------------------------------- 1 | 114 | 115 | 116 | 117 | 118 |
119 |

{seo_config.title}

120 |

{seo_config.description}

121 | 122 | 123 |
124 | -------------------------------------------------------------------------------- /apps/web/src/routes/news-article/+page.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | export const load = async () => { 4 | const slug = 'news-article-copy'; 5 | try { 6 | const Copy = await import(`../../lib/copy/${slug}.md`); 7 | return { 8 | Copy: Copy.default, 9 | }; 10 | } catch (e) { 11 | throw error(404, 'Uh oh!'); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/src/routes/web-page/+page.svelte: -------------------------------------------------------------------------------- 1 | 83 | 84 | 85 | 86 | 87 |
88 |

{seo_config.title}

89 |

{seo_config.description}

90 | 91 | 92 |
93 | -------------------------------------------------------------------------------- /apps/web/src/routes/web-page/+page.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | export const load = async () => { 4 | const slug = 'web-page-copy'; 5 | try { 6 | const Copy = await import(`../../lib/copy/${slug}.md`); 7 | return { 8 | Copy: Copy.default, 9 | }; 10 | } catch (e) { 11 | throw error(404, 'Uh oh!'); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spences10/svead/90e440d196068f7731af6107eebffb8825f06e10/apps/web/static/favicon.png -------------------------------------------------------------------------------- /apps/web/static/spencee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spences10/svead/90e440d196068f7731af6107eebffb8825f06e10/apps/web/static/spencee.png -------------------------------------------------------------------------------- /apps/web/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | import { mdsvex } from 'mdsvex'; 4 | import mdsvexConfig from './mdsvex.config.js'; 5 | 6 | /** @type {import('@sveltejs/kit').Config} */ 7 | const config = { 8 | extensions: ['.svelte', ...mdsvexConfig.extensions], 9 | 10 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 11 | // for more information about preprocessors 12 | preprocess: [mdsvex(mdsvexConfig), vitePreprocess({})], 13 | 14 | kit: { 15 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 16 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 17 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 18 | adapter: adapter(), 19 | }, 20 | }; 21 | 22 | export default config; 23 | -------------------------------------------------------------------------------- /apps/web/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const daisyui = require('daisyui'); 2 | const typography = require('@tailwindcss/typography'); 3 | 4 | /** @type {import('tailwindcss').Config}*/ 5 | const config = { 6 | content: ['./src/**/*.{html,js,svelte,ts}'], 7 | 8 | theme: { 9 | extend: {}, 10 | }, 11 | 12 | plugins: [typography, daisyui], 13 | 14 | daisyui: { 15 | themes: ['night', 'winter'], 16 | }, 17 | }; 18 | 19 | module.exports = config; 20 | -------------------------------------------------------------------------------- /apps/web/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | let pageURL = 'http://localhost:4173/'; 3 | 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/'); 6 | }); 7 | 8 | test.afterEach(async ({ page }, testInfo) => { 9 | if (testInfo.status === 'failed') { 10 | await page.screenshot({ 11 | path: `screenshots/${testInfo.title.replace(/\s+/g, '_')}.png`, 12 | }); 13 | } 14 | }); 15 | 16 | test('index page has h1', async ({ page }) => { 17 | await page.waitForSelector('h1'); 18 | expect(await page.textContent('h1')).toBe('Welcome to Svead 🍺'); 19 | }); 20 | 21 | test.describe('meta tags', () => { 22 | test('head has canonical', async ({ page }) => { 23 | const metaCanonical = page.locator('link[rel="canonical"]'); 24 | await expect(metaCanonical.first()).toHaveAttribute( 25 | 'href', 26 | pageURL, 27 | ); 28 | }); 29 | 30 | test('head has description', async ({ page }) => { 31 | const metaDescription = page.locator('meta[name="description"]'); 32 | await expect(metaDescription.first()).toHaveAttribute( 33 | 'content', 34 | 'Svead, a component that allows you to set head meta information, canonical, title, Twitter and Facebook Open Graph tags.', 35 | ); 36 | }); 37 | 38 | test('has open graph title', async ({ page }) => { 39 | const metaOgTitle = page.locator('meta[property="og:title"]'); 40 | await expect(metaOgTitle.first()).toHaveAttribute( 41 | 'content', 42 | 'This is Svead a Svelte Head Component', 43 | ); 44 | }); 45 | 46 | test('has open graph description', async ({ page }) => { 47 | const metaOgDescription = page.locator( 48 | 'meta[property="og:description"]', 49 | ); 50 | await expect(metaOgDescription.first()).toHaveAttribute( 51 | 'content', 52 | 'Svead, a component that allows you to set head meta information, canonical, title, Twitter and Facebook Open Graph tags.', 53 | ); 54 | }); 55 | 56 | test('has open graph image', async ({ page }) => { 57 | const metaOgImage = page.locator('meta[property="og:image"]'); 58 | await expect(metaOgImage.first()).toHaveAttribute( 59 | 'content', 60 | 'https://og.tailgraph.com/og?fontFamily=Roboto&title=This+is+Svead&titleTailwind=text-gray-800+font-bold+text-6xl&text=Set+Head+meta+tag+information&textTailwind=text-gray-700+text-2xl+mt-4&logoTailwind=h-8&bgTailwind=bg-white&footer=svead.pages.dev&footerTailwind=text-teal-600', 61 | ); 62 | }); 63 | 64 | test('has open graph url', async ({ page }) => { 65 | const metaOgUrl = page.locator('meta[property="og:url"]'); 66 | await expect(metaOgUrl.first()).toHaveAttribute( 67 | 'content', 68 | pageURL, 69 | ); 70 | }); 71 | 72 | test('has twitter card', async ({ page }) => { 73 | const metaTwitterCard = page.locator('meta[name="twitter:card"]'); 74 | await expect(metaTwitterCard.first()).toHaveAttribute( 75 | 'content', 76 | 'summary_large_image', 77 | ); 78 | }); 79 | 80 | test('has twitter title', async ({ page }) => { 81 | const metaTwitterTitle = page.locator( 82 | 'meta[name="twitter:title"]', 83 | ); 84 | await expect(metaTwitterTitle.first()).toHaveAttribute( 85 | 'content', 86 | 'This is Svead a Svelte Head Component', 87 | ); 88 | }); 89 | 90 | test('has twitter description', async ({ page }) => { 91 | const metaTwitterDescription = page.locator( 92 | 'meta[name="twitter:description"]', 93 | ); 94 | await expect(metaTwitterDescription.first()).toHaveAttribute( 95 | 'content', 96 | 'Svead, a component that allows you to set head meta information, canonical, title, Twitter and Facebook Open Graph tags.', 97 | ); 98 | }); 99 | 100 | test('has twitter image', async ({ page }) => { 101 | const metaTwitterImage = page.locator( 102 | 'meta[name="twitter:image"]', 103 | ); 104 | await expect(metaTwitterImage.first()).toHaveAttribute( 105 | 'content', 106 | 'https://og.tailgraph.com/og?fontFamily=Roboto&title=This+is+Svead&titleTailwind=text-gray-800+font-bold+text-6xl&text=Set+Head+meta+tag+information&textTailwind=text-gray-700+text-2xl+mt-4&logoTailwind=h-8&bgTailwind=bg-white&footer=svead.pages.dev&footerTailwind=text-teal-600', 107 | ); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // 16 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 17 | // from the referenced tsconfig.json - TypeScript does not merge them in 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | test: { 7 | environment: 'jsdom', 8 | include: ['src/**/*.{test,spec}.{js,ts}'], 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /apps/web/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, mergeConfig } from 'vitest/config'; 2 | import viteConfig from './vite.config'; 3 | 4 | export default mergeConfig( 5 | viteConfig, 6 | defineConfig({ 7 | test: { 8 | include: ['src/**/*.test.{js,ts,svelte}'], 9 | globals: true, 10 | environment: 'jsdom', 11 | coverage: { 12 | all: false, 13 | thresholds: { 14 | statements: 75, 15 | branches: 84, 16 | functions: 68, 17 | lines: 75, 18 | }, 19 | }, 20 | }, 21 | }), 22 | ); 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev:web": "pnpm run build:packages && pnpm --filter=web dev", 5 | "build:web": "pnpm run build:packages && pnpm --filter=web build", 6 | "preview:web": "pnpm run build:packages && pnpm --filter=web preview", 7 | "test:unit:web": "pnpm run build:packages && pnpm --filter=web test:unit", 8 | "test:int:web": "pnpm run build:packages && pnpm --filter=web test:integration", 9 | "coverage:web": "pnpm run build:packages && pnpm --filter=web coverage", 10 | "dev:svead": "pnpm run build:packages && pnpm --filter=svead dev", 11 | "build:svead": "pnpm run build:packages && pnpm --filter=svead build", 12 | "preview:svead": "pnpm run build:packages && pnpm --filter=svead preview", 13 | "test:unit:svead": "pnpm run build:packages && pnpm --filter=svead test:unit", 14 | "test:int:svead": "pnpm run build:packages && pnpm --filter=svead test:integration", 15 | "coverage:svead": "pnpm run build:packages && pnpm --filter=svead coverage", 16 | "build:packages": "pnpm -r --filter=\"./packages/*\" run build", 17 | "format": "pnpm -r --filter=\"./apps/*\" --filter=\"./packages/*\" run format", 18 | "lint": "pnpm -r --filter=\"./apps/*\" --filter=\"./packages/*\" run lint", 19 | "changeset": "changeset", 20 | "version": "changeset version", 21 | "release": "pnpm run build:packages && changeset publish" 22 | }, 23 | "devDependencies": { 24 | "@changesets/cli": "^2.27.12" 25 | } 26 | } -------------------------------------------------------------------------------- /packages/svead/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /dist 5 | /.svelte-kit 6 | /package 7 | .env 8 | .env.* 9 | !.env.example 10 | vite.config.js.timestamp-* 11 | vite.config.ts.timestamp-* 12 | -------------------------------------------------------------------------------- /packages/svead/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /packages/svead/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore files for PNPM, NPM and YARN 2 | pnpm-lock.yaml 3 | package-lock.json 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /packages/svead/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 70, 6 | "arrowParens": "avoid", 7 | "proseWrap": "always", 8 | "plugins": ["prettier-plugin-svelte"], 9 | "overrides": [ 10 | { "files": "*.svelte", "options": { "parser": "svelte" } } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/svead/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # svead 2 | 3 | ## 0.0.14 4 | 5 | ### Patch Changes 6 | 7 | - add changeset 8 | -------------------------------------------------------------------------------- /packages/svead/README.md: -------------------------------------------------------------------------------- 1 | # Svead 🍺 - Svelte Head Component 2 | 3 | [docs](../../README.md) -------------------------------------------------------------------------------- /packages/svead/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import ts from 'typescript-eslint'; 3 | import svelte from 'eslint-plugin-svelte'; 4 | import prettier from 'eslint-config-prettier'; 5 | import globals from 'globals'; 6 | 7 | /** @type {import('eslint').Linter.Config[]} */ 8 | export default [ 9 | js.configs.recommended, 10 | ...ts.configs.recommended, 11 | ...svelte.configs['flat/recommended'], 12 | prettier, 13 | ...svelte.configs['flat/prettier'], 14 | { 15 | languageOptions: { 16 | globals: { 17 | ...globals.browser, 18 | ...globals.node 19 | } 20 | } 21 | }, 22 | { 23 | files: ['**/*.svelte'], 24 | languageOptions: { 25 | parserOptions: { 26 | parser: ts.parser 27 | } 28 | } 29 | }, 30 | { 31 | ignores: ['build/', '.svelte-kit/', 'dist/'] 32 | } 33 | ]; 34 | -------------------------------------------------------------------------------- /packages/svead/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svead", 3 | "version": "0.0.14", 4 | "author": { 5 | "name": "Scott Spence", 6 | "email": "svead@scottspence.dev", 7 | "url": "https://scottspence.com" 8 | }, 9 | "keywords": [ 10 | "svelte", 11 | "sveltekit", 12 | "head", 13 | "meta-tags", 14 | "seo", 15 | "social", 16 | "twitter", 17 | "facebook", 18 | "og", 19 | "open-graph", 20 | "schema", 21 | "json-ld", 22 | "jsonld", 23 | "structured-data" 24 | ], 25 | "scripts": { 26 | "dev": "vite dev", 27 | "build": "vite build && npm run package", 28 | "preview": "vite preview", 29 | "package": "svelte-kit sync && svelte-package && publint", 30 | "prepublishOnly": "npm run package", 31 | "test": "npm run test:integration && npm run test:unit", 32 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 33 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 34 | "lint": "prettier --check . && eslint .", 35 | "format": "prettier --write .", 36 | "test:integration": "playwright test", 37 | "test:unit": "vitest", 38 | "test:ci": "vitest run", 39 | "coverage": "vitest run --coverage" 40 | }, 41 | "exports": { 42 | ".": { 43 | "types": "./dist/index.d.ts", 44 | "svelte": "./dist/index.js" 45 | } 46 | }, 47 | "files": [ 48 | "dist", 49 | "!dist/**/*.test.*", 50 | "!dist/**/*.spec.*" 51 | ], 52 | "peerDependencies": { 53 | "svelte": "^4.0.0 || ^5.0.0" 54 | }, 55 | "devDependencies": { 56 | "@playwright/test": "^1.50.1", 57 | "@sveltejs/adapter-auto": "^4.0.0", 58 | "@sveltejs/kit": "^2.17.1", 59 | "@sveltejs/package": "^2.3.10", 60 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 61 | "@testing-library/svelte": "^5.2.6", 62 | "@types/eslint": "9.6.1", 63 | "@typescript-eslint/eslint-plugin": "^8.23.0", 64 | "@typescript-eslint/parser": "^8.23.0", 65 | "@vitest/coverage-v8": "^3.0.5", 66 | "eslint": "^9.19.0", 67 | "eslint-config-prettier": "^10.0.1", 68 | "eslint-plugin-svelte": "^2.46.1", 69 | "jsdom": "^26.0.0", 70 | "prettier": "^3.4.2", 71 | "prettier-plugin-svelte": "^3.3.3", 72 | "publint": "^0.3.2", 73 | "schema-dts": "^1.1.2", 74 | "svelte": "5.26.2", 75 | "svelte-check": "^4.1.4", 76 | "tslib": "^2.8.1", 77 | "typescript": "^5.7.3", 78 | "vite": "^6.0.11", 79 | "vitest": "^3.0.5" 80 | }, 81 | "svelte": "./dist/index.js", 82 | "types": "./dist/index.d.ts", 83 | "type": "module", 84 | "repository": { 85 | "type": "git", 86 | "url": "git+https://github.com/spences10/svead.git" 87 | }, 88 | "license": "MIT", 89 | "bugs": { 90 | "url": "https://github.com/spences10/svead/issues" 91 | }, 92 | "homepage": "https://github.com/spences10/svead#readme" 93 | } 94 | -------------------------------------------------------------------------------- /packages/svead/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | 3 | const config: PlaywrightTestConfig = { 4 | webServer: { 5 | command: 'pnpm run build && pnpm run preview', 6 | port: 4173, 7 | }, 8 | testDir: 'tests', 9 | testMatch: /(.+\.)?(test|spec)\.[jt]s/, 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /packages/svead/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /packages/svead/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | %sveltekit.head% 11 | 12 | 13 |
%sveltekit.body%
14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/svead/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/svead/src/lib/components/head.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | {seo_config.title} 12 | 13 | 14 | 15 | {#if seo_config.author_name} 16 | 17 | {/if} 18 | 19 | 20 | 21 | 22 | 23 | 24 | {#if seo_config.open_graph_image} 25 | 26 | 27 | {/if} 28 | {#if seo_config.site_name} 29 | 30 | {/if} 31 | 32 | 33 | 34 | 35 | 36 | {#if seo_config.open_graph_image} 37 | 38 | {/if} 39 | {#if seo_config.twitter_handle} 40 | 41 | {/if} 42 | {#if seo_config.website} 43 | 44 | 45 | {/if} 46 | 47 | 48 | 49 | 50 | {#if seo_config.open_graph_image} 51 | 52 | {/if} 53 | 54 | 55 | {#if seo_config.payment_pointer} 56 | 57 | {/if} 58 | 59 | 60 | {#if seo_config.language} 61 | 62 | {/if} 63 | -------------------------------------------------------------------------------- /packages/svead/src/lib/components/head.test.ts: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/svelte/svelte5'; 2 | import { afterEach, describe, expect, it } from 'vitest'; 3 | import Head from './head.svelte'; 4 | 5 | const clean_html_content = (content: string): string => { 6 | return content.replace(//g, ''); 7 | }; 8 | 9 | describe('Head component tests', () => { 10 | afterEach(() => { 11 | document.head.innerHTML = ''; 12 | }); 13 | 14 | describe('Basic Meta Tags', () => { 15 | it('renders canonical link correctly', async () => { 16 | const url = 'https://example.com'; 17 | render(Head, { 18 | seo_config: { 19 | url, 20 | title: 'Test', 21 | description: 'Test description', 22 | }, 23 | }); 24 | const canonical = document.querySelector( 25 | 'link[rel="canonical"]', 26 | ); 27 | expect(canonical?.getAttribute('href')).toBe(url); 28 | }); 29 | 30 | it('renders the correct title, description, and author', async () => { 31 | const config = { 32 | title: 'Test Title', 33 | description: 'Test Description', 34 | author_name: 'Test Author', 35 | url: 'https://example.com', 36 | }; 37 | render(Head, { seo_config: config }); 38 | 39 | expect(document.title).toBe(config.title); 40 | expect( 41 | document 42 | .querySelector('meta[name="description"]') 43 | ?.getAttribute('content'), 44 | ).toBe(config.description); 45 | expect( 46 | document 47 | .querySelector('meta[name="author"]') 48 | ?.getAttribute('content'), 49 | ).toBe(config.author_name); 50 | }); 51 | 52 | it('renders the monetization tag when a payment pointer is provided', async () => { 53 | const payment_pointer = '$wallet.example.com/alice'; 54 | render(Head, { 55 | seo_config: { 56 | title: 'Test', 57 | description: 'Test', 58 | url: 'https://example.com', 59 | payment_pointer, 60 | }, 61 | }); 62 | const monetization = document.querySelector( 63 | 'meta[name="monetization"]', 64 | ); 65 | expect(monetization?.getAttribute('content')).toBe( 66 | payment_pointer, 67 | ); 68 | }); 69 | }); 70 | 71 | describe('Open Graph Protocol', () => { 72 | it('renders Open Graph tags correctly', async () => { 73 | const config = { 74 | title: 'OG Title', 75 | description: 'OG Description', 76 | url: 'https://example.com', 77 | open_graph_image: 'https://example.com/image.jpg', 78 | site_name: 'Example Site', 79 | language: 'en-US', 80 | }; 81 | render(Head, { seo_config: config }); 82 | 83 | expect( 84 | document 85 | .querySelector('meta[property="og:title"]') 86 | ?.getAttribute('content'), 87 | ).toBe(config.title); 88 | expect( 89 | document 90 | .querySelector('meta[property="og:description"]') 91 | ?.getAttribute('content'), 92 | ).toBe(config.description); 93 | expect( 94 | document 95 | .querySelector('meta[property="og:url"]') 96 | ?.getAttribute('content'), 97 | ).toBe(config.url); 98 | expect( 99 | document 100 | .querySelector('meta[property="og:image"]') 101 | ?.getAttribute('content'), 102 | ).toBe(config.open_graph_image); 103 | expect( 104 | document 105 | .querySelector('meta[property="og:site_name"]') 106 | ?.getAttribute('content'), 107 | ).toBe(config.site_name); 108 | expect( 109 | document 110 | .querySelector('meta[property="og:locale"]') 111 | ?.getAttribute('content'), 112 | ).toBe(config.language); 113 | }); 114 | }); 115 | 116 | describe('Twitter Card', () => { 117 | it('renders Twitter Card tags correctly', async () => { 118 | const config = { 119 | title: 'Twitter Title', 120 | description: 'Twitter Description', 121 | url: 'https://example.com', 122 | open_graph_image: 'https://example.com/image.jpg', 123 | website: 'example.com', 124 | twitter_handle: '@example', 125 | twitter_card_type: 'summary_large_image' as const, 126 | }; 127 | render(Head, { seo_config: config }); 128 | 129 | expect( 130 | document 131 | .querySelector('meta[name="twitter:title"]') 132 | ?.getAttribute('content'), 133 | ).toBe(config.title); 134 | expect( 135 | document 136 | .querySelector('meta[name="twitter:description"]') 137 | ?.getAttribute('content'), 138 | ).toBe(config.description); 139 | expect( 140 | document 141 | .querySelector('meta[name="twitter:image"]') 142 | ?.getAttribute('content'), 143 | ).toBe(config.open_graph_image); 144 | expect( 145 | document 146 | .querySelector('meta[property="twitter:domain"]') 147 | ?.getAttribute('content'), 148 | ).toBe(config.website); 149 | expect( 150 | document 151 | .querySelector('meta[name="twitter:creator"]') 152 | ?.getAttribute('content'), 153 | ).toBe(config.twitter_handle); 154 | expect( 155 | document 156 | .querySelector('meta[name="twitter:card"]') 157 | ?.getAttribute('content'), 158 | ).toBe(config.twitter_card_type); 159 | }); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /packages/svead/src/lib/components/schema-org.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | 30 | 31 | {@html json_ld_data} 32 | 33 | -------------------------------------------------------------------------------- /packages/svead/src/lib/components/schema-org.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, describe, it } from 'vitest'; 2 | 3 | describe.skip('SchemaOrg', () => { 4 | afterEach(() => { 5 | document.head.innerHTML = ''; 6 | }); 7 | 8 | it.skip('head loads correctly', () => {}); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/svead/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Head } from './components/head.svelte'; 2 | export { default as SchemaOrg } from './components/schema-org.svelte'; 3 | export type { SchemaOrgProps, SeoConfig } from './types/index.js'; 4 | -------------------------------------------------------------------------------- /packages/svead/src/lib/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './schema-org.js'; 2 | export * from './seo-config.js'; 3 | -------------------------------------------------------------------------------- /packages/svead/src/lib/types/schema-org.ts: -------------------------------------------------------------------------------- 1 | import type { Thing, WithContext } from 'schema-dts'; 2 | 3 | export type SchemaOrgType = Thing | WithContext; 4 | 5 | export interface SchemaOrgProps { 6 | schema: SchemaOrgType | SchemaOrgType[]; 7 | } 8 | -------------------------------------------------------------------------------- /packages/svead/src/lib/types/seo-config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SEO configuration options for a web page. 3 | * Covers basic SEO, Open Graph, and Twitter Cards requirements. 4 | * Use this interface to define the SEO configuration for a webpage, 5 | * including meta tags and social media properties. 6 | * The properties in this interface can be used to generate the 7 | * necessary HTML meta tags. 8 | */ 9 | export interface SeoConfig { 10 | /** 11 | * The title of the web page. 12 | * Used in the tag, og:title, and twitter:title properties. 13 | * 14 | * @type {string} 15 | */ 16 | title: string; 17 | 18 | /** 19 | * The description of the web page. 20 | * Used in the description meta tag, og:description, and 21 | * twitter:description properties. 22 | * 23 | * Best practices suggest keeping the description between 50-160 characters. 24 | * Search engines may truncate descriptions longer than 155-160 characters. 25 | * 26 | * Note: The Head component does not enforce these limits, 27 | * it's up to the developer to ensure appropriate length. 28 | * 29 | * @type {string} 30 | */ 31 | description: string; 32 | 33 | /** 34 | * The URL of the web page. 35 | * Used as the og:url property and twitter:url. 36 | * 37 | * @type {string} 38 | */ 39 | url: string; 40 | 41 | /** 42 | * The website to which the web page belongs. 43 | * Used as twitter:domain. 44 | * 45 | * @type {string} 46 | */ 47 | website?: string; 48 | 49 | /** 50 | * The language of the web page. 51 | * Used as the og:locale property. 52 | * Defaults to 'en'. 53 | * 54 | * @type {string} 55 | * @default 'en' 56 | */ 57 | language?: string; 58 | 59 | /** 60 | * The URL of the Open Graph image for the web page. 61 | * Used as the og:image and twitter:image properties. 62 | * 63 | * @type {string} 64 | */ 65 | open_graph_image?: string; 66 | 67 | /** 68 | * The payment pointer for Web Monetization. 69 | * Used in the monetization meta tag. 70 | * 71 | * @type {string} 72 | */ 73 | payment_pointer?: string; 74 | 75 | /** 76 | * The name of the author of the web page. 77 | * Used in the author meta tag. 78 | * 79 | * @type {string} 80 | */ 81 | author_name?: string; 82 | 83 | /** 84 | * The name of the site. 85 | * Used as the og:site_name property. 86 | * 87 | * @type {string} 88 | */ 89 | site_name?: string; 90 | 91 | /** 92 | * The Twitter handle of the content creator or site. 93 | * Used as the twitter:creator property. 94 | * Should include the @ symbol. 95 | * 96 | * @type {string} 97 | */ 98 | twitter_handle?: string; 99 | 100 | /** 101 | * The type of Twitter card to use. 102 | * Used as the twitter:card property. 103 | * Defaults to 'summary_large_image'. 104 | * 105 | * @type {'summary' | 'summary_large_image' | 'app' | 'player'} 106 | * @default 'summary_large_image' 107 | */ 108 | twitter_card_type?: 109 | | 'summary' 110 | | 'summary_large_image' 111 | | 'app' 112 | | 'player'; 113 | 114 | /** 115 | * Alternative text for the Open Graph image. 116 | * Used as the og:image:alt property. 117 | * 118 | * @type {string} 119 | */ 120 | open_graph_image_alt?: string; 121 | } 122 | -------------------------------------------------------------------------------- /packages/svead/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | <h1>Welcome to your library project</h1> 2 | <p> 3 | Create your package using @sveltejs/package and preview/showcase 4 | your work with SvelteKit 5 | </p> 6 | <p> 7 | Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the 8 | documentation 9 | </p> 10 | -------------------------------------------------------------------------------- /packages/svead/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spences10/svead/90e440d196068f7731af6107eebffb8825f06e10/packages/svead/static/favicon.png -------------------------------------------------------------------------------- /packages/svead/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 14 | adapter: adapter(), 15 | }, 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /packages/svead/tests/test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('index page has expected h1', async ({ page }) => { 4 | await page.goto('/'); 5 | await expect( 6 | page.getByRole('heading', { 7 | name: 'Welcome to your library project', 8 | }), 9 | ).toBeVisible(); 10 | }); -------------------------------------------------------------------------------- /packages/svead/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "module": "NodeNext", 13 | "moduleResolution": "NodeNext" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/svead/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { svelteTesting } from '@testing-library/svelte/vite'; 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | export default defineConfig({ 6 | plugins: [sveltekit(), svelteTesting()], 7 | test: { 8 | environment: 'jsdom', 9 | include: ['src/**/*.{test,spec}.{js,ts}'], 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/svead/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, mergeConfig } from 'vitest/config'; 2 | import viteConfig from './vite.config'; 3 | 4 | export default mergeConfig( 5 | viteConfig, 6 | defineConfig({ 7 | test: { 8 | include: ['src/**/*.test.{js,ts,svelte}'], 9 | globals: true, 10 | environment: 'jsdom', 11 | coverage: { 12 | all: false, 13 | thresholds: { 14 | statements: 75, 15 | branches: 84, 16 | functions: 68, 17 | lines: 75, 18 | }, 19 | }, 20 | }, 21 | }), 22 | ); 23 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'apps/*' 3 | - 'packages/*' 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"] 3 | } 4 | --------------------------------------------------------------------------------