├── demo ├── src │ ├── env.d.ts │ ├── pages │ │ ├── mdx.mdx │ │ ├── tweet │ │ │ └── [id].astro │ │ └── index.astro │ ├── layouts │ │ └── BaseLayout.astro │ └── components │ │ └── Header.astro ├── tsconfig.json ├── public │ ├── social.png │ └── favicon.svg ├── README.md ├── .gitignore ├── astro.config.mjs ├── tailwind.config.mjs ├── package.json └── CHANGELOG.md ├── pnpm-workspace.yaml ├── packages └── astro-tweet │ ├── tsconfig.json │ ├── index.ts │ ├── src │ ├── twitter-theme │ │ ├── tweet-skeleton.module.css │ │ ├── Skeleton.astro │ │ ├── AvatarImg.astro │ │ ├── MediaImg.astro │ │ ├── quoted-tweet │ │ │ ├── index.ts │ │ │ ├── QuotedTweetContainer.astro │ │ │ ├── QuotedTweetBody.astro │ │ │ ├── quoted-tweet-body.module.css │ │ │ ├── quoted-tweet-container.module.css │ │ │ ├── QuotedTweet.astro │ │ │ ├── quoted-tweet-header.module.css │ │ │ └── QuotedTweetHeader.astro │ │ ├── icons │ │ │ ├── icons.module.css │ │ │ ├── index.ts │ │ │ ├── Verified.astro │ │ │ ├── VerifiedGovernment.astro │ │ │ └── VerifiedBusiness.astro │ │ ├── tweet-not-found.module.css │ │ ├── tweet-link.module.css │ │ ├── TweetLink.astro │ │ ├── tweet-body.module.css │ │ ├── verified-badge.module.css │ │ ├── tweet-info-created-at.module.css │ │ ├── tweet-in-reply-to.module.css │ │ ├── TweetNotFound.astro │ │ ├── TweetContainer.astro │ │ ├── TweetInReplyTo.astro │ │ ├── skeleton.module.css │ │ ├── TweetInfoCreatedAt.astro │ │ ├── types.ts │ │ ├── TweetSkeleton.astro │ │ ├── TweetReplies.astro │ │ ├── tweet-container.module.css │ │ ├── TweetMediaVideo.astro │ │ ├── components.ts │ │ ├── tweet-replies.module.css │ │ ├── tweet-info.module.css │ │ ├── TweetBody.astro │ │ ├── tweet-media.module.css │ │ ├── VerifiedBadge.astro │ │ ├── TweetInfo.astro │ │ ├── EmbeddedTweet.astro │ │ ├── tweet-media-video.module.css │ │ ├── TweetActions.astro │ │ ├── tweet-header.module.css │ │ ├── tweet-actions.module.css │ │ ├── TweetHeader.astro │ │ ├── TweetMedia.astro │ │ └── theme.css │ ├── api │ │ ├── index.ts │ │ ├── types │ │ │ ├── edit.ts │ │ │ ├── index.ts │ │ │ ├── photo.ts │ │ │ ├── user.ts │ │ │ ├── video.ts │ │ │ ├── entities.ts │ │ │ ├── media.ts │ │ │ └── tweet.ts │ │ ├── get-oembed.ts │ │ └── get-tweet.ts │ ├── AstroTweet.astro │ ├── Tweet.astro │ ├── TweetContent.astro │ └── utils.ts │ ├── CHANGELOG.md │ ├── package.json │ └── README.md ├── .vscode ├── settings.json └── extensions.json ├── .changeset ├── config.json └── README.md ├── .gitignore ├── .github └── workflows │ ├── changeset-version.js │ └── release.yml ├── package.json ├── CONTRIBUTING.md ├── LICENSE └── README.md /demo/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict" 3 | } -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "demo" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /packages/astro-tweet/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict" 3 | } 4 | -------------------------------------------------------------------------------- /demo/public/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsriram/astro-tweet/HEAD/demo/public/social.png -------------------------------------------------------------------------------- /packages/astro-tweet/index.ts: -------------------------------------------------------------------------------- 1 | import AstroTweet from "./src/Tweet.astro"; 2 | 3 | export default AstroTweet; 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "spellright.language": [], 3 | "spellright.documentTypes": ["latex", "plaintext"] 4 | } 5 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # astro-tweet demo 2 | 3 | A simple Astro site to demo 4 | [astro-tweet](https://github.com/tsriram/astro-tweet) 5 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/tweet-skeleton.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | pointer-events: none; 3 | padding-bottom: 0.25rem; 4 | } 5 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types/index.js"; 2 | export * from "./get-tweet.js"; 3 | export * from "./get-oembed.js"; 4 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/Skeleton.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import styles from "./skeleton.module.css"; 3 | --- 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/AstroTweet.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Write your component code in this file! 3 | interface Props { 4 | prefix?: string; 5 | } 6 | --- 7 | 8 |
{Astro.props.prefix} My special component
9 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/api/types/edit.ts: -------------------------------------------------------------------------------- 1 | export interface TweetEditControl { 2 | edit_tweet_ids: string[]; 3 | editable_until_msecs: string; 4 | is_edit_eligible: boolean; 5 | edits_remaining: string; 6 | } 7 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/AvatarImg.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | src: string; 4 | alt: string; 5 | width: number; 6 | height: number; 7 | } 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/MediaImg.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | src: string; 4 | alt: string; 5 | class?: string; 6 | draggable?: boolean; 7 | } 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/quoted-tweet/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./QuotedTweet.astro"; 2 | export * from "./QuotedTweetContainer.astro"; 3 | export * from "./QuotedTweetHeader.astro"; 4 | export * from "./quoted-tweet-body.astro"; 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "astro-build.astro-vscode", 4 | "esbenp.prettier-vscode", 5 | "editorconfig.editorconfig", 6 | "dbaeumer.vscode-eslint" 7 | ], 8 | "unwantedRecommendations": [] 9 | } 10 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/api/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./edit.js"; 2 | export * from "./entities.js"; 3 | export * from "./media.js"; 4 | export * from "./photo.js"; 5 | export * from "./tweet.js"; 6 | export * from "./user.js"; 7 | export * from "./video.js"; 8 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/icons/icons.module.css: -------------------------------------------------------------------------------- 1 | .verified { 2 | margin-left: 0.125rem; 3 | max-width: 20px; 4 | max-height: 20px; 5 | height: 1.25em; 6 | fill: currentColor; 7 | user-select: none; 8 | vertical-align: text-bottom; 9 | } 10 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/tweet-not-found.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | padding-bottom: 0.75rem; 6 | } 7 | .root > h3 { 8 | font-size: 1.25rem; 9 | margin-bottom: 0.5rem; 10 | } 11 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/icons/index.ts: -------------------------------------------------------------------------------- 1 | import Verified from "./Verified.astro"; 2 | import VerifiedBusiness from "./VerifiedBusiness.astro"; 3 | import VerifiedGovernment from "./VerifiedGovernment.astro"; 4 | 5 | export { Verified, VerifiedBusiness, VerifiedGovernment }; 6 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/api/types/photo.ts: -------------------------------------------------------------------------------- 1 | import type { Rect, RGB } from "./media.js"; 2 | 3 | export interface TweetPhoto { 4 | backgroundColor: RGB; 5 | cropCandidates: Rect[]; 6 | expandedUrl: string; 7 | url: string; 8 | width: number; 9 | height: number; 10 | } 11 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/tweet-link.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | font-weight: inherit; 3 | color: var(--tweet-color-blue-secondary); 4 | text-decoration: none; 5 | cursor: pointer; 6 | } 7 | .root:hover { 8 | text-decoration-thickness: 1px; 9 | text-decoration-line: underline; 10 | } 11 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/TweetLink.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import s from "./tweet-link.module.css"; 3 | interface Props { 4 | href: string; 5 | } 6 | --- 7 | 8 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/tweet-body.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | font-size: var(--tweet-body-font-size); 3 | font-weight: var(--tweet-body-font-weight); 4 | line-height: var(--tweet-body-line-height); 5 | margin: var(--tweet-body-margin); 6 | overflow-wrap: break-word; 7 | white-space: pre-wrap; 8 | } 9 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/verified-badge.module.css: -------------------------------------------------------------------------------- 1 | .verifiedOld { 2 | color: var(--tweet-verified-old-color); 3 | } 4 | .verifiedBlue { 5 | color: var(--tweet-verified-blue-color); 6 | } 7 | .verifiedGovernment { 8 | /* color: var(--tweet-verified-government-color); */ 9 | color: rgb(130, 154, 171); 10 | } 11 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/tweet-info-created-at.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | color: inherit; 3 | text-decoration: none; 4 | font-size: var(--tweet-info-font-size); 5 | line-height: var(--tweet-info-line-height); 6 | } 7 | .root:hover { 8 | text-decoration-thickness: 1px; 9 | text-decoration-line: underline; 10 | } 11 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/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 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/api/types/user.ts: -------------------------------------------------------------------------------- 1 | export interface TweetUser { 2 | id_str: string; 3 | name: string; 4 | profile_image_url_https: string; 5 | profile_image_shape: "Circle" | "Square"; 6 | screen_name: string; 7 | verified: boolean; 8 | verified_type?: "Business" | "Government"; 9 | is_blue_verified: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/api/get-oembed.ts: -------------------------------------------------------------------------------- 1 | export async function getOEmbed(url: string): Promise { 2 | const res = await fetch(`https://publish.twitter.com/oembed?url=${url}`); 3 | 4 | if (res.ok) return res.json(); 5 | if (res.status === 404) return; 6 | 7 | throw new Error(`Fetch for embedded tweet failed with code: ${res.status}`); 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | # Vercel 24 | .vercel -------------------------------------------------------------------------------- /demo/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import vercel from "@astrojs/vercel/serverless"; 2 | import { defineConfig } from "astro/config"; 3 | import tailwind from "@astrojs/tailwind"; 4 | import mdx from "@astrojs/mdx"; 5 | 6 | // https://astro.build/config 7 | export default defineConfig({ 8 | integrations: [mdx(), tailwind()], 9 | adapter: vercel(), 10 | output: "static", 11 | }); 12 | -------------------------------------------------------------------------------- /demo/src/pages/mdx.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | layout: ../layouts/BaseLayout.astro 3 | --- 4 | 5 | import AstroTweet from "astro-tweet"; 6 | 7 |

8 | 13 | astro-tweet 14 | 15 | MDX Demo 16 |

17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/quoted-tweet/QuotedTweetContainer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { EnrichedQuotedTweet } from "../../utils.js"; 3 | import styles from "./quoted-tweet-container.module.css"; 4 | 5 | type Props = { tweet: EnrichedQuotedTweet }; 6 | --- 7 | 8 |
9 |
10 | 11 |
12 |
13 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/quoted-tweet/QuotedTweetBody.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { EnrichedQuotedTweet } from "../../utils.js"; 3 | import styles from "./quoted-tweet-body.module.css"; 4 | 5 | interface Props { 6 | tweet: EnrichedQuotedTweet; 7 | } 8 | --- 9 | 10 |

11 | {Astro.props.tweet.entities.map((item, i) => {item.text})} 12 |

13 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/quoted-tweet/quoted-tweet-body.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | font-size: var(--tweet-quoted-body-font-size); 3 | font-weight: var(--tweet-quoted-body-font-weight); 4 | line-height: var(--tweet-quoted-body-line-height); 5 | margin: var(--tweet-quoted-body-margin); 6 | overflow-wrap: break-word; 7 | white-space: pre-wrap; 8 | padding: 0 0.75rem; 9 | } 10 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/Tweet.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import TweetContent from "./TweetContent.astro"; 3 | 4 | interface Props { 5 | id: string; 6 | // TODO: add support for custom components (do we need this though?) 7 | // components?: TwitterComponents; 8 | fetchOptions?: RequestInit; 9 | onError?: ((error: any) => any) | undefined; 10 | } 11 | --- 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/tweet-in-reply-to.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | text-decoration: none; 3 | color: var(--tweet-font-color-secondary); 4 | font-size: 0.9375rem; 5 | line-height: 1.25rem; 6 | margin-bottom: 0.25rem; 7 | overflow-wrap: break-word; 8 | white-space: pre-wrap; 9 | } 10 | .root:hover { 11 | text-decoration-thickness: 1px; 12 | text-decoration-line: underline; 13 | } 14 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/TweetNotFound.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import TweetContainer from "./TweetContainer.astro"; 3 | import styles from "./tweet-not-found.module.css"; 4 | 5 | interface Props { 6 | error?: any; 7 | } 8 | --- 9 | 10 | 11 |
12 |

Tweet not found

13 |

The embedded tweet could not be found…

14 |
15 |
16 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/api/types/video.ts: -------------------------------------------------------------------------------- 1 | export interface TweetVideo { 2 | aspectRatio: [number, number]; 3 | contentType: string; 4 | durationMs: number; 5 | mediaAvailability: { 6 | status: string; 7 | }; 8 | poster: string; 9 | variants: { 10 | type: string; 11 | src: string; 12 | }[]; 13 | videoId: { 14 | type: string; 15 | id: string; 16 | }; 17 | viewCount: number; 18 | } 19 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/TweetContainer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import clsx from "clsx"; 3 | import styles from "./tweet-container.module.css"; 4 | import "./theme.css"; 5 | 6 | interface Props { 7 | className?: string; 8 | } 9 | 10 | const { className } = Astro.props; 11 | --- 12 | 13 |
14 |
15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/TweetInReplyTo.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { EnrichedTweet } from "../utils.js"; 3 | import s from "./tweet-in-reply-to.module.css"; 4 | interface Props { 5 | tweet: EnrichedTweet; 6 | } 7 | const { tweet } = Astro.props; 8 | --- 9 | 10 | 16 | Replying to @{tweet.in_reply_to_screen_name} 17 | 18 | -------------------------------------------------------------------------------- /demo/tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], 4 | darkMode: "media", 5 | theme: { 6 | extend: { 7 | colors: { 8 | light: { 9 | bg: "#f0ebd8", 10 | text: "#1b2836" 11 | }, 12 | dark: { 13 | bg: "#243447", 14 | text: "#fff" 15 | } 16 | } 17 | } 18 | }, 19 | plugins: [] 20 | }; 21 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/quoted-tweet/quoted-tweet-container.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | overflow: hidden; 4 | border: var(--tweet-border); 5 | border-radius: 12px; 6 | margin: var(--tweet-quoted-container-margin); 7 | transition-property: background-color, box-shadow; 8 | transition-duration: 0.2s; 9 | /* cursor: pointer; */ 10 | } 11 | 12 | .root:hover { 13 | background-color: var(--tweet-quoted-bg-color-hover); 14 | } 15 | 16 | .article { 17 | position: relative; 18 | box-sizing: inherit; 19 | } 20 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/skeleton.module.css: -------------------------------------------------------------------------------- 1 | .skeleton { 2 | display: block; 3 | width: 100%; 4 | border-radius: 5px; 5 | background-image: var(--tweet-skeleton-gradient); 6 | background-size: 400% 100%; 7 | animation: loading 8s ease-in-out infinite; 8 | } 9 | 10 | @media (prefers-reduced-motion: reduce) { 11 | .skeleton { 12 | animation: none; 13 | background-position: 200% 0; 14 | } 15 | } 16 | 17 | @keyframes loading { 18 | 0% { 19 | background-position: 200% 0; 20 | } 21 | 100% { 22 | background-position: -200% 0; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/changeset-version.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require("node:child_process"); 2 | 3 | // This script is used by the `release.yml` workflow to update the version of the packages being released. 4 | // The standard step is only to run `changeset version` but this does not update the package-lock.json file. 5 | // So we also run `npm install`, which does this update. 6 | // This is a workaround until this is handled automatically by `changeset version`. 7 | // See https://github.com/changesets/changesets/issues/421. 8 | execSync("pnpm changeset version"); 9 | execSync("pnpm install"); 10 | -------------------------------------------------------------------------------- /packages/astro-tweet/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # astro-tweet 2 | 3 | ## 0.0.6 4 | 5 | ### Patch Changes 6 | 7 | - d4c5d21: Handle TweetTombstone result coming from Twitter API. 8 | 9 | ## 0.0.5 10 | 11 | ### Patch Changes 12 | 13 | - 12f7500: Support for Astro 5 14 | 15 | ## 0.0.4 16 | 17 | ### Patch Changes 18 | 19 | - d824959: Add support for Astro 4 20 | 21 | ## 0.0.3 22 | 23 | ### Patch Changes 24 | 25 | - ebcb65a: fix issue with filename case in git that was blocking the build 26 | 27 | ## 0.0.2 28 | 29 | ### Patch Changes 30 | 31 | - 7f9c942: Initial release 32 | 33 | ## 0.0.1 34 | 35 | ### Patch Changes 36 | 37 | - 7e2a8f0: Initial release 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-tweet-monorepo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "check": "pnpm -r prepublishOnly && pnpm -r check", 8 | "format": "pnpm -r format", 9 | "changeset:version": "pnpm changeset version && pnpm -r generate:version && git add --all", 10 | "changeset:release": "pnpm changeset publish" 11 | }, 12 | "keywords": [], 13 | "author": "Sriram Thiagarajan", 14 | "license": "ISC", 15 | "engines": { 16 | "pnpm": "^8.10.0" 17 | }, 18 | "packageManager": "pnpm@8.10.0", 19 | "dependencies": { 20 | "@changesets/cli": "^2.26.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Local development 2 | 3 | This project is a monorepo set up using 4 | [pnpm Workspace](https://pnpm.io/workspaces). To run / develop this project 5 | locally: 6 | 7 | 1. Clone this repo or your fork 8 | 2. Run `pnpm install` from the root directory 9 | 3. Run `pnpm dev` from the `/demo` directory to run the demo site 10 | 4. You can make changes to any component in `packages/astro-tweet` and see the 11 | changes in the demo page 12 | 5. Once you have the changes ready, run `pnpm changeset add` if there's any 13 | changes in the `astro-tweet` package. If your changes do not require 14 | publishing a new version to `npm`, you don't need to do this step. 15 | -------------------------------------------------------------------------------- /demo/src/pages/tweet/[id].astro: -------------------------------------------------------------------------------- 1 | --- 2 | export const prerender = false; 3 | const { id } = Astro.params; 4 | import Tweet from "astro-tweet"; 5 | import BaseLayout from "../../layouts/BaseLayout.astro"; 6 | --- 7 | 8 | 9 |

10 | 15 | astro-tweet SSR demo 17 |

18 | 19 |
20 | This tweet was rendered on the server. Try changing the ID in the URL to see 21 | a different tweet. 22 |
23 |
24 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/TweetInfoCreatedAt.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import format from "date-fns/format/index.js"; 3 | import type { EnrichedTweet } from "../utils.js"; 4 | import styles from "./tweet-info-created-at.module.css"; 5 | 6 | interface Props { 7 | tweet: EnrichedTweet; 8 | } 9 | const { tweet } = Astro.props; 10 | const createdAt = new Date(tweet.created_at); 11 | --- 12 | 13 | 20 | 23 | 24 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "type": "module", 4 | "version": "0.0.6", 5 | "private": true, 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro check && astro build", 10 | "preview": "astro preview", 11 | "astro": "astro" 12 | }, 13 | "dependencies": { 14 | "@astrojs/check": "^0.9.4", 15 | "@astrojs/mdx": "^4.3.0", 16 | "@astrojs/node": "^9.3.0", 17 | "@astrojs/tailwind": "^6.0.2", 18 | "@astrojs/vercel": "^8.2.1", 19 | "astro": "^5.11.0", 20 | "astro-tweet": "workspace:*", 21 | "tailwindcss": "^3.0.24", 22 | "typescript": "^5.8.3" 23 | }, 24 | "packageManager": "pnpm@8.10.0" 25 | } 26 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom components that the default Twitter theme allows. 3 | * 4 | * Note: We only use these components in Server Components, because the root `Tweet` 5 | * component that uses them is a Server Component and you can't pass down functions to a 6 | * client component unless they're Server Actions. 7 | */ 8 | export type TwitterComponents = { 9 | TweetNotFound?: typeof import("./TweetNotFound.astro"); 10 | AvatarImg?: typeof import("./AvatarImg.astro"); 11 | MediaImg?: typeof import("./MediaImg.astro"); 12 | }; 13 | 14 | /** 15 | * @deprecated Use `TwitterComponents` instead. 16 | */ 17 | export type TweetComponents = TwitterComponents; 18 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/quoted-tweet/QuotedTweet.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { EnrichedQuotedTweet } from "../../utils"; 3 | import QuotedTweetContainer from "./QuotedTweetContainer.astro"; 4 | import QuotedTweetHeader from "./QuotedTweetHeader.astro"; 5 | import QuotedTweetBody from "./QuotedTweetBody.astro"; 6 | import TweetMedia from "../TweetMedia.astro"; 7 | 8 | interface Props { 9 | tweet: EnrichedQuotedTweet; 10 | } 11 | const { tweet } = Astro.props; 12 | --- 13 | 14 | 15 | 16 | 17 | {tweet.mediaDetails?.length ? : null} 18 | 19 | -------------------------------------------------------------------------------- /demo/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # demo 2 | 3 | ## 0.0.6 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [d4c5d21] 8 | - astro-tweet@0.0.6 9 | 10 | ## 0.0.5 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [12f7500] 15 | - astro-tweet@0.0.5 16 | 17 | ## 0.0.4 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [d824959] 22 | - astro-tweet@0.0.4 23 | 24 | ## 0.0.3 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [ebcb65a] 29 | - astro-tweet@0.0.3 30 | 31 | ## 0.0.2 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies [7f9c942] 36 | - astro-tweet@0.0.2 37 | 38 | ## 0.0.1 39 | 40 | ### Patch Changes 41 | 42 | - Updated dependencies [7e2a8f0] 43 | - astro-tweet@0.0.1 44 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/TweetSkeleton.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import TweetContainer from "./TweetContainer.astro"; 3 | import Skeleton from "./Skeleton.astro"; 4 | import styles from "./tweet-skeleton.module.css"; 5 | --- 6 | 7 | 8 | 9 | 10 |
11 | 16 | 23 |
24 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/TweetReplies.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { type EnrichedTweet, formatNumber } from "../utils.js"; 3 | import s from "./tweet-replies.module.css"; 4 | 5 | interface Props { 6 | tweet: EnrichedTweet; 7 | } 8 | const { tweet } = Astro.props; 9 | --- 10 | 11 | 24 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/icons/Verified.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import styles from "./icons.module.css"; 3 | --- 4 | 5 | 11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/tweet-container.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | min-width: 250px; 4 | max-width: 550px; 5 | overflow: hidden; 6 | /* Base font styles */ 7 | color: var(--tweet-font-color); 8 | font-family: var(--tweet-font-family); 9 | font-weight: 400; 10 | box-sizing: border-box; 11 | border: var(--tweet-border); 12 | border-radius: 12px; 13 | margin: var(--tweet-container-margin); 14 | background-color: var(--tweet-bg-color); 15 | transition-property: background-color, box-shadow; 16 | transition-duration: 0.2s; 17 | } 18 | .root:hover { 19 | background-color: var(--tweet-bg-color-hover); 20 | } 21 | .article { 22 | position: relative; 23 | box-sizing: inherit; 24 | padding: 0.75rem 1rem; 25 | } 26 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/TweetMediaVideo.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { MediaAnimatedGif, MediaVideo } from "../api/index.js"; 3 | import { 4 | type EnrichedQuotedTweet, 5 | type EnrichedTweet, 6 | getMediaUrl, 7 | getMp4Video, 8 | } from "../utils.js"; 9 | import mediaStyles from "./tweet-media.module.css"; 10 | 11 | interface Props { 12 | tweet: EnrichedTweet | EnrichedQuotedTweet; 13 | media: MediaAnimatedGif | MediaVideo; 14 | } 15 | 16 | const { media } = Astro.props; 17 | const mp4Video = getMp4Video(media); 18 | --- 19 | 20 | 29 | -------------------------------------------------------------------------------- /demo/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/components.ts: -------------------------------------------------------------------------------- 1 | export * from "./types.js"; 2 | export * from "./icons/index.js"; 3 | export * from "./EmbeddedTweet.astro"; 4 | // export * from "./TweetActionsCopy.astro"; 5 | export * from "./TweetActions.astro"; 6 | export * from "./TweetBody.astro"; 7 | export * from "./TweetContainer.astro"; 8 | export * from "./TweetHeader.astro"; 9 | export * from "./TweetInReplyTo.astro"; 10 | export * from "./TweetInfoCreatedAt.astro"; 11 | export * from "./TweetInfo.astro"; 12 | export * from "./TweetLink.astro"; 13 | export * from "./TweetMediaVideo.astro"; 14 | export * from "./tweet-media.astro"; 15 | export * from "./TweetNotFound.astro"; 16 | export * from "./TweetReplies.astro"; 17 | export * from "./TweetSkeleton.astro"; 18 | export * from "./quoted-tweet/index.js"; 19 | -------------------------------------------------------------------------------- /packages/astro-tweet/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-tweet", 3 | "version": "0.0.6", 4 | "type": "module", 5 | "exports": { 6 | ".": "./index.ts" 7 | }, 8 | "files": [ 9 | "src", 10 | "index.ts" 11 | ], 12 | "keywords": [ 13 | "astro-component", 14 | "withastro", 15 | "astro", 16 | "twitter" 17 | ], 18 | "scripts": { 19 | "format": "prettier --write . --plugin=prettier-plugin-astro" 20 | }, 21 | "devDependencies": { 22 | "astro": "^3.4.0", 23 | "prettier": "^3.0.3", 24 | "prettier-plugin-astro": "^0.12.1" 25 | }, 26 | "peerDependencies": { 27 | "astro": "^3.0.0 || ^4.0.0 || ^5.0.0" 28 | }, 29 | "dependencies": { 30 | "@astrojs/check": "^0.2.1", 31 | "clsx": "^2.0.0", 32 | "date-fns": "^2.30.0", 33 | "typescript": "^5.2.2" 34 | } 35 | } -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/quoted-tweet/quoted-tweet-header.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | padding: 0.75rem 0.75rem 0 0.75rem; 4 | line-height: var(--tweet-header-line-height); 5 | font-size: var(--tweet-header-font-size); 6 | white-space: nowrap; 7 | overflow-wrap: break-word; 8 | overflow: hidden; 9 | } 10 | 11 | .avatar { 12 | position: relative; 13 | height: 20px; 14 | width: 20px; 15 | } 16 | 17 | .avatarSquare { 18 | border-radius: 4px; 19 | } 20 | 21 | .author { 22 | display: flex; 23 | margin: 0 0.5rem; 24 | } 25 | 26 | .authorText { 27 | font-weight: 700; 28 | text-overflow: ellipsis; 29 | overflow: hidden; 30 | white-space: nowrap; 31 | } 32 | 33 | .username { 34 | color: var(--tweet-font-color-secondary); 35 | text-decoration: none; 36 | text-overflow: ellipsis; 37 | margin-left: 0.125rem; 38 | } 39 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/api/types/entities.ts: -------------------------------------------------------------------------------- 1 | export type Indices = [number, number]; 2 | 3 | export interface HashtagEntity { 4 | indices: Indices; 5 | text: string; 6 | } 7 | 8 | export interface UserMentionEntity { 9 | id_str: string; 10 | indices: Indices; 11 | name: string; 12 | screen_name: string; 13 | } 14 | 15 | export interface MediaEntity { 16 | display_url: string; 17 | expanded_url: string; 18 | indices: Indices; 19 | url: string; 20 | } 21 | 22 | export interface UrlEntity { 23 | display_url: string; 24 | expanded_url: string; 25 | indices: Indices; 26 | url: string; 27 | } 28 | 29 | export interface SymbolEntity { 30 | indices: Indices; 31 | text: string; 32 | } 33 | 34 | export interface TweetEntities { 35 | hashtags: HashtagEntity[]; 36 | urls: UrlEntity[]; 37 | user_mentions: UserMentionEntity[]; 38 | symbols: SymbolEntity[]; 39 | media?: MediaEntity[]; 40 | } 41 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/tweet-replies.module.css: -------------------------------------------------------------------------------- 1 | .replies { 2 | padding: 0.25rem 0; 3 | } 4 | .link { 5 | text-decoration: none; 6 | color: var(--tweet-color-blue-secondary); 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | min-width: 32px; 11 | min-height: 32px; 12 | user-select: none; 13 | outline-style: none; 14 | transition-property: background-color; 15 | transition-duration: 0.2s; 16 | padding: 0 1rem; 17 | border: var(--tweet-border); 18 | border-radius: 9999px; 19 | } 20 | .link:hover { 21 | background-color: var(--tweet-color-blue-secondary-hover); 22 | } 23 | .text { 24 | font-weight: var(--tweet-replies-font-weight); 25 | font-size: var(--tweet-replies-font-size); 26 | line-height: var(--tweet-replies-line-height); 27 | overflow-wrap: break-word; 28 | white-space: nowrap; 29 | text-overflow: ellipsis; 30 | overflow: hidden; 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import AstroTweet from "astro-tweet"; 3 | import BaseLayout from "../layouts/BaseLayout.astro"; 4 | --- 5 | 6 | 7 |

8 | 13 | astro-tweet static page demo 15 |

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | This tweet was statically generated. For a demo of server rendered page, 26 | visit this page. 29 | Also works with MDX. 32 |
33 |
34 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/icons/VerifiedGovernment.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import styles from "./icons.module.css"; 3 | --- 4 | 5 | 11 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/tweet-info.module.css: -------------------------------------------------------------------------------- 1 | .info { 2 | display: flex; 3 | align-items: center; 4 | color: var(--tweet-font-color-secondary); 5 | margin-top: 0.125rem; 6 | overflow-wrap: break-word; 7 | white-space: nowrap; 8 | text-overflow: ellipsis; 9 | } 10 | .infoLink { 11 | color: inherit; 12 | text-decoration: none; 13 | } 14 | .infoLink { 15 | height: var(--tweet-actions-icon-wrapper-size); 16 | width: var(--tweet-actions-icon-wrapper-size); 17 | font: inherit; 18 | margin-left: auto; 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | margin-right: -4px; 23 | border-radius: 9999px; 24 | transition-property: background-color; 25 | transition-duration: 0.2s; 26 | } 27 | .infoLink:hover { 28 | background-color: var(--tweet-color-blue-secondary-hover); 29 | } 30 | .infoIcon { 31 | color: inherit; 32 | fill: currentColor; 33 | height: var(--tweet-actions-icon-size); 34 | user-select: none; 35 | } 36 | .infoLink:hover > .infoIcon { 37 | color: var(--tweet-color-blue-secondary); 38 | } 39 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/TweetBody.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { EnrichedTweet } from "../utils.js"; 3 | import TweetLink from "./TweetLink.astro"; 4 | import styles from "./tweet-body.module.css"; 5 | interface Props { 6 | tweet: EnrichedTweet; 7 | } 8 | const { tweet } = Astro.props; 9 | --- 10 | 11 |

12 | { 13 | tweet.entities.map((item, i) => { 14 | switch (item.type) { 15 | case "hashtag": 16 | case "mention": 17 | case "url": 18 | case "symbol": 19 | return {item.text}; 20 | case "media": 21 | // Media text is currently never displayed, some tweets however might have indices 22 | // that do match `display_text_range` so for those cases we ignore the content. 23 | return; 24 | default: 25 | // We use `dangerouslySetInnerHTML` to preserve the text encoding. 26 | // https://github.com/vercel-labs/react-tweet/issues/29 27 | return ; 28 | } 29 | }) 30 | } 31 |

32 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/tweet-media.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-top: 0.75rem; 3 | overflow: hidden; 4 | position: relative; 5 | } 6 | .rounded { 7 | border: var(--tweet-border); 8 | border-radius: 12px; 9 | } 10 | .mediaWrapper { 11 | display: grid; 12 | grid-auto-rows: 1fr; 13 | gap: 2px; 14 | height: 100%; 15 | width: 100%; 16 | } 17 | .grid2Columns { 18 | grid-template-columns: repeat(2, 1fr); 19 | } 20 | .grid3 > a:first-child { 21 | grid-row: span 2; 22 | } 23 | .grid2x2 { 24 | grid-template-rows: repeat(2, 1fr); 25 | } 26 | .mediaContainer { 27 | position: relative; 28 | height: 100%; 29 | width: 100%; 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | } 34 | .mediaLink { 35 | text-decoration: none; 36 | outline-style: none; 37 | } 38 | .skeleton { 39 | padding-bottom: 56.25%; 40 | width: 100%; 41 | display: block; 42 | } 43 | .image { 44 | position: absolute; 45 | top: 0px; 46 | left: 0px; 47 | bottom: 0px; 48 | height: 100%; 49 | width: 100%; 50 | margin: 0; 51 | object-fit: cover; 52 | object-position: center; 53 | } 54 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/VerifiedBadge.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import clsx from "clsx"; 3 | import type { TweetUser } from "../api/index.js"; 4 | import { Verified, VerifiedBusiness, VerifiedGovernment } from "./icons/index"; 5 | import styles from "./verified-badge.module.css"; 6 | 7 | interface Props { 8 | user: TweetUser; 9 | className?: string; 10 | } 11 | 12 | const { user, className } = Astro.props; 13 | const verified = user.verified || user.is_blue_verified || user.verified_type; 14 | let Icon = Verified; 15 | let iconClassName: string | null = styles.verifiedBlue; 16 | 17 | if (verified) { 18 | if (!user.is_blue_verified) { 19 | iconClassName = styles.verifiedOld; 20 | } 21 | switch (user.verified_type) { 22 | case "Government": 23 | Icon = VerifiedGovernment; 24 | iconClassName = styles.verifiedGovernment; 25 | break; 26 | case "Business": 27 | Icon = VerifiedBusiness; 28 | iconClassName = null; 29 | break; 30 | } 31 | } 32 | --- 33 | 34 | { 35 | verified ? ( 36 |
37 | 38 |
39 | ) : null 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sriram Thiagarajan 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 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/TweetInfo.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { EnrichedTweet } from "../utils.js"; 3 | import TweetInfoCreatedAt from "./TweetInfoCreatedAt.astro"; 4 | import styles from "./tweet-info.module.css"; 5 | 6 | interface Props { 7 | tweet: EnrichedTweet; 8 | } 9 | const { tweet } = Astro.props; 10 | --- 11 | 12 | 30 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/TweetContent.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getTweet } from "./api"; 3 | import EmbeddedTweet from "./twitter-theme/EmbeddedTweet.astro"; 4 | import TweetNotFound from "./twitter-theme/TweetNotFound.astro"; 5 | import type { TwitterComponents } from "./twitter-theme/types"; 6 | interface Props { 7 | id: string; 8 | components?: TwitterComponents; 9 | fetchOptions?: RequestInit; 10 | onError?: ((error: any) => any) | undefined; 11 | } 12 | 13 | const { id, components, fetchOptions, onError } = Astro.props; 14 | let error; 15 | const tweet = id 16 | ? await getTweet(id, fetchOptions).catch((err) => { 17 | if (onError) { 18 | error = onError(err); 19 | } else { 20 | console.error(err); 21 | error = err; 22 | } 23 | }) 24 | : undefined; 25 | let tweetNotFound = false; 26 | // TODO: typing 27 | let NotFound: any; 28 | if (!tweet) { 29 | NotFound = components?.TweetNotFound || TweetNotFound; 30 | tweetNotFound = true; 31 | } 32 | --- 33 | 34 | { 35 | tweetNotFound ? ( 36 | 37 | ) : ( 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: {} 9 | concurrency: ${{ github.workflow }}-${{ github.ref }} 10 | 11 | jobs: 12 | release: 13 | name: Release 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | 18 | if: ${{ github.repository_owner == 'tsriram' }} 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout Repo 22 | uses: actions/checkout@v3 23 | with: 24 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 25 | fetch-depth: 0 26 | 27 | - uses: pnpm/action-setup@v4 28 | - name: Setup Node.js 22.x 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: 22.x 32 | cache: pnpm 33 | 34 | - name: Install Dependencies 35 | run: pnpm install --frozen-lockfile 36 | 37 | - name: Create Release Pull Request or Publish to npm 38 | id: changesets 39 | uses: changesets/action@v1 40 | with: 41 | publish: pnpm changeset publish 42 | version: pnpm changeset version 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 46 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/quoted-tweet/QuotedTweetHeader.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import clsx from "clsx"; 3 | import AvatarImg from "../AvatarImg.astro"; 4 | import styles from "./quoted-tweet-header.module.css"; 5 | import type { EnrichedQuotedTweet } from "../../utils.js"; 6 | import VerifiedBadge from "../VerifiedBadge.astro"; 7 | 8 | interface Props { 9 | tweet: EnrichedQuotedTweet; 10 | } 11 | const { user } = Astro.props.tweet; 12 | const { tweet } = Astro.props; 13 | --- 14 | 15 |
16 | 22 |
28 | 34 |
35 |
36 |
37 |
38 | {user.name} 39 |
40 | 41 |
42 | @{user.screen_name} 43 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/EmbeddedTweet.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { Tweet } from "../api/index.js"; 3 | import type { TwitterComponents } from "./types.js"; 4 | import TweetContainer from "./TweetContainer.astro"; 5 | import TweetHeader from "./TweetHeader.astro"; 6 | import TweetInReplyTo from "./TweetInReplyTo.astro"; 7 | import TweetBody from "./TweetBody.astro"; 8 | import TweetMedia from "./TweetMedia.astro"; 9 | import TweetInfo from "./TweetInfo.astro"; 10 | import TweetActions from "./TweetActions.astro"; 11 | import TweetReplies from "./TweetReplies.astro"; 12 | import QuotedTweet from "./quoted-tweet/QuotedTweet.astro"; 13 | import { enrichTweet } from "../utils.js"; 14 | 15 | type Props = { 16 | tweet: Tweet; 17 | components?: Omit; 18 | }; 19 | 20 | const { tweet: t, components } = Astro.props; 21 | const tweet = enrichTweet(t); 22 | --- 23 | 24 | 25 | 26 | {tweet.in_reply_to_status_id_str && } 27 | 28 | { 29 | tweet.mediaDetails?.length ? ( 30 | 31 | ) : null 32 | } 33 | {tweet.quoted_tweet && } 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/api/types/media.ts: -------------------------------------------------------------------------------- 1 | import type { Indices } from "./entities.js"; 2 | 3 | export type RGB = { 4 | red: number; 5 | green: number; 6 | blue: number; 7 | }; 8 | 9 | export type Rect = { 10 | x: number; 11 | y: number; 12 | w: number; 13 | h: number; 14 | }; 15 | 16 | export type Size = { 17 | h: number; 18 | w: number; 19 | resize: string; 20 | }; 21 | 22 | export interface VideoInfo { 23 | aspect_ratio: [number, number]; 24 | variants: { 25 | bitrate?: number; 26 | content_type: "video/mp4" | "application/x-mpegURL"; 27 | url: string; 28 | }[]; 29 | } 30 | 31 | interface MediaBase { 32 | display_url: string; 33 | expanded_url: string; 34 | ext_media_availability: { 35 | status: string; 36 | }; 37 | ext_media_color: { 38 | palette: { 39 | percentage: number; 40 | rgb: RGB; 41 | }[]; 42 | }; 43 | indices: Indices; 44 | media_url_https: string; 45 | original_info: { 46 | height: number; 47 | width: number; 48 | focus_rects: Rect[]; 49 | }; 50 | sizes: { 51 | large: Size; 52 | medium: Size; 53 | small: Size; 54 | thumb: Size; 55 | }; 56 | url: string; 57 | } 58 | 59 | export interface MediaPhoto extends MediaBase { 60 | type: "photo"; 61 | ext_alt_text?: string; 62 | } 63 | 64 | export interface MediaAnimatedGif extends MediaBase { 65 | type: "animated_gif"; 66 | video_info: VideoInfo; 67 | } 68 | 69 | export interface MediaVideo extends MediaBase { 70 | type: "video"; 71 | video_info: VideoInfo; 72 | } 73 | 74 | export type MediaDetails = MediaPhoto | MediaAnimatedGif | MediaVideo; 75 | -------------------------------------------------------------------------------- /packages/astro-tweet/README.md: -------------------------------------------------------------------------------- 1 | # astro-tweet 2 | 3 | Embed tweets in your Astro sites with zero JavaScript on the client side. This 4 | is a (slightly opinionated) port of the fantastic 5 | [react-tweet](https://github.com/vercel/react-tweet) library. Huge thanks to 6 | Vercel and all the contributors of react-tweet. 7 | 8 | HTML for the tweets are generated at the build time and there is no JavaScript 9 | bundled. This uses Twitter's 10 | 11 | ## How to use? 12 | 13 | ### Install 14 | 15 | ``` 16 | npm i astro-tweet 17 | ``` 18 | 19 | In your Astro component: 20 | 21 | ```js 22 | --- 23 | import Tweet from "astro-tweet"; 24 | --- 25 | 26 | 27 | 28 | ``` 29 | 30 | Where `id` is the tweet ID. For example, in 31 | https://twitter.com/tsriram/status/1713443573168034170, `1713443573168034170` is 32 | the ID. 33 | 34 | This also works with MDX files in Astro. 35 | 36 | ### Major changes from react-tweet 37 | 38 | This library ports almost all of the features of 39 | [react-tweet](https://github.com/vercel/react-tweet) except the below. These are 40 | mainly omitted since they'd require adding client side JavaScript but the 41 | features are probably not "must haves" IMO. This may change in the future. 42 | 43 | 1. `Copy link` option (rendered next to likes & reply) is not available. 44 | 2. Video player doesn't have custom play button rendered in the center of the 45 | video element (like it does in twitter apps). Instead you get a native HTML 46 | video element. 47 | 48 | ## Running demo locally 49 | 50 | 1. Clone the repo 51 | 2. Run `pnpm install` from the project directory 52 | 3. Run `pnpm dev` from the `demo` directory 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # astro-tweet 2 | 3 | Embed tweets in your Astro sites with zero JavaScript on the client side. This 4 | is a (slightly opinionated) port of the fantastic 5 | [react-tweet](https://github.com/vercel/react-tweet) library. Huge thanks to 6 | Vercel and all the contributors of react-tweet. 7 | 8 | HTML for the tweets are generated at the build time and there is no JavaScript 9 | bundled. This uses Twitter's 10 | 11 | ## How to use? 12 | 13 | ### Install 14 | 15 | ``` 16 | npm i astro-tweet 17 | ``` 18 | 19 | In your Astro component: 20 | 21 | ```js 22 | --- 23 | import Tweet from "astro-tweet"; 24 | --- 25 | 26 | 27 | 28 | ``` 29 | 30 | Where `id` is the tweet ID. For example, in 31 | https://twitter.com/tsriram/status/1713443573168034170, `1713443573168034170` is 32 | the ID. 33 | 34 | This also works with MDX files in Astro. 35 | 36 | ### Major changes from react-tweet 37 | 38 | This library ports almost all of the features of 39 | [react-tweet](https://github.com/vercel/react-tweet) except the below. These are 40 | mainly omitted since they'd require adding client side JavaScript but the 41 | features are probably not "must haves" IMO. This may change in the future. If 42 | you'd like these changed, please create an issue to discuss. 43 | 44 | 1. `Copy link` option (rendered next to likes & reply) is not available. 45 | 2. Video player doesn't have custom play button rendered in the center of the 46 | video element (like it does in twitter apps). Instead you get a native HTML 47 | video element. 48 | 49 | ## Running demo locally 50 | 51 | 1. Clone the repo 52 | 2. Run `pnpm install` from the project directory 53 | 3. Run `pnpm dev` from the `demo` directory 54 | 55 | ## Contributing 56 | 57 | All kinds of contributions are welcome. For code contributions / local setup, 58 | please check [CONTRIBUTING.md](CONTRIBUTING.md) 59 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/tweet-media-video.module.css: -------------------------------------------------------------------------------- 1 | .anchor { 2 | display: flex; 3 | align-items: center; 4 | color: white; 5 | padding: 0 1rem; 6 | border: 1px solid transparent; 7 | border-radius: 9999px; 8 | font-weight: 700; 9 | transition: background-color 0.2s; 10 | cursor: pointer; 11 | user-select: none; 12 | outline-style: none; 13 | text-decoration: none; 14 | text-overflow: ellipsis; 15 | white-space: nowrap; 16 | } 17 | .videoButton { 18 | position: relative; 19 | height: 67px; 20 | width: 67px; 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | background-color: var(--tweet-color-blue-primary); 25 | transition-property: background-color; 26 | transition-duration: 0.2s; 27 | border: 4px solid #fff; 28 | border-radius: 9999px; 29 | cursor: pointer; 30 | } 31 | .videoButton:hover, 32 | .videoButton:focus-visible { 33 | background-color: var(--tweet-color-blue-primary-hover); 34 | } 35 | .videoButtonIcon { 36 | margin-left: 3px; 37 | width: calc(50% + 4px); 38 | height: calc(50% + 4px); 39 | max-width: 100%; 40 | color: #fff; 41 | fill: currentColor; 42 | user-select: none; 43 | } 44 | .watchOnTwitter { 45 | position: absolute; 46 | top: 12px; 47 | right: 8px; 48 | } 49 | .watchOnTwitter > a { 50 | min-width: 2rem; 51 | min-height: 2rem; 52 | font-size: 0.875rem; 53 | line-height: 1rem; 54 | backdrop-filter: blur(4px); 55 | background-color: rgba(15, 20, 25, 0.75); 56 | } 57 | .watchOnTwitter > a:hover { 58 | background-color: rgba(39, 44, 48, 0.75); 59 | } 60 | .viewReplies { 61 | position: relative; 62 | min-height: 2rem; 63 | background-color: var(--tweet-color-blue-primary); 64 | border-color: var(--tweet-color-blue-primary); 65 | font-size: 0.9375rem; 66 | line-height: 1.25rem; 67 | } 68 | .viewReplies:hover { 69 | background-color: var(--tweet-color-blue-primary-hover); 70 | } 71 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/TweetActions.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { type EnrichedTweet, formatNumber } from "../utils.js"; 3 | // import { TweetActionsCopy } from './tweet-actions-copy.jsx' 4 | import styles from "./tweet-actions.module.css"; 5 | 6 | interface Props { 7 | tweet: EnrichedTweet; 8 | } 9 | 10 | const { tweet } = Astro.props; 11 | const favoriteCount = formatNumber(tweet.favorite_count); 12 | --- 13 | 14 | 53 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/icons/VerifiedBusiness.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import styles from "./icons.module.css"; 3 | --- 4 | 5 | 11 | 12 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 34 | 35 | 36 | 37 | 38 | 39 | 42 | 45 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/tweet-header.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | padding-bottom: 0.75rem; 4 | line-height: var(--tweet-header-line-height); 5 | font-size: var(--tweet-header-font-size); 6 | white-space: nowrap; 7 | overflow-wrap: break-word; 8 | overflow: hidden; 9 | } 10 | 11 | .avatar { 12 | position: relative; 13 | height: 48px; 14 | width: 48px; 15 | } 16 | .avatarOverflow { 17 | height: 100%; 18 | width: 100%; 19 | position: absolute; 20 | overflow: hidden; 21 | border-radius: 9999px; 22 | } 23 | .avatarSquare { 24 | border-radius: 4px; 25 | } 26 | .avatarShadow { 27 | height: 100%; 28 | width: 100%; 29 | transition-property: background-color; 30 | transition-duration: 0.2s; 31 | box-shadow: rgb(0 0 0 / 3%) 0px 0px 2px inset; 32 | } 33 | .avatarShadow:hover { 34 | background-color: rgba(26, 26, 26, 0.15); 35 | } 36 | 37 | .author { 38 | max-width: calc(100% - 84px); 39 | display: flex; 40 | flex-direction: column; 41 | justify-content: center; 42 | margin: 0 0.5rem; 43 | } 44 | .authorLink { 45 | text-decoration: none; 46 | color: inherit; 47 | display: flex; 48 | align-items: center; 49 | } 50 | .authorLink:hover { 51 | text-decoration-line: underline; 52 | } 53 | .authorVerified { 54 | display: inline-flex; 55 | } 56 | .authorLinkText { 57 | font-weight: 700; 58 | text-overflow: ellipsis; 59 | overflow: hidden; 60 | white-space: nowrap; 61 | } 62 | 63 | .authorMeta { 64 | display: flex; 65 | } 66 | .authorFollow { 67 | display: flex; 68 | } 69 | .username { 70 | color: var(--tweet-font-color-secondary); 71 | text-decoration: none; 72 | text-overflow: ellipsis; 73 | } 74 | .follow { 75 | color: var(--tweet-color-blue-secondary); 76 | text-decoration: none; 77 | font-weight: 700; 78 | } 79 | .follow:hover { 80 | text-decoration-line: underline; 81 | } 82 | .separator { 83 | padding: 0 0.25rem; 84 | } 85 | 86 | .brand { 87 | margin-inline-start: auto; 88 | } 89 | 90 | .twitterIcon { 91 | width: 23.75px; 92 | height: 23.75px; 93 | color: var(--tweet-twitter-icon-color); 94 | fill: currentColor; 95 | user-select: none; 96 | } 97 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/tweet-actions.module.css: -------------------------------------------------------------------------------- 1 | .actions { 2 | display: flex; 3 | align-items: center; 4 | color: var(--tweet-font-color-secondary); 5 | padding-top: 0.25rem; 6 | margin-top: 0.25rem; 7 | border-top: var(--tweet-border); 8 | overflow-wrap: break-word; 9 | white-space: nowrap; 10 | text-overflow: ellipsis; 11 | } 12 | 13 | .like, 14 | .reply, 15 | .copy { 16 | text-decoration: none; 17 | color: inherit; 18 | display: flex; 19 | align-items: center; 20 | margin-right: 1.25rem; 21 | } 22 | .like:hover, 23 | .reply:hover, 24 | .copy:hover { 25 | background-color: rgba(0, 0, 0, 0); 26 | } 27 | .like:hover > .likeIconWrapper { 28 | background-color: var(--tweet-color-red-primary-hover); 29 | } 30 | .like:hover > .likeCount { 31 | color: var(--tweet-color-red-primary); 32 | text-decoration-line: underline; 33 | } 34 | .likeIconWrapper, 35 | .replyIconWrapper, 36 | .copyIconWrapper { 37 | width: var(--tweet-actions-icon-wrapper-size); 38 | height: var(--tweet-actions-icon-wrapper-size); 39 | display: flex; 40 | justify-content: center; 41 | align-items: center; 42 | margin-left: -0.25rem; 43 | border-radius: 9999px; 44 | } 45 | .likeIcon, 46 | .replyIcon, 47 | .copyIcon { 48 | height: var(--tweet-actions-icon-size); 49 | fill: currentColor; 50 | user-select: none; 51 | } 52 | .likeIcon { 53 | color: var(--tweet-color-red-primary); 54 | } 55 | .likeCount, 56 | .replyText, 57 | .copyText { 58 | font-size: var(--tweet-actions-font-size); 59 | font-weight: var(--tweet-actions-font-weight); 60 | line-height: var(--tweet-actions-line-height); 61 | margin-left: 0.25rem; 62 | } 63 | 64 | .reply:hover > .replyIconWrapper { 65 | background-color: var(--tweet-color-blue-secondary-hover); 66 | } 67 | .reply:hover > .replyText { 68 | color: var(--tweet-color-blue-secondary); 69 | text-decoration-line: underline; 70 | } 71 | .replyIcon { 72 | color: var(--tweet-color-blue-primary); 73 | } 74 | 75 | .copy { 76 | font: inherit; 77 | background: none; 78 | border: none; 79 | cursor: pointer; 80 | } 81 | .copy:hover > .copyIconWrapper { 82 | background-color: var(--tweet-color-green-primary-hover); 83 | } 84 | .copy:hover .copyIcon { 85 | color: var(--tweet-color-green-primary); 86 | } 87 | .copy:hover > .copyText { 88 | color: var(--tweet-color-green-primary); 89 | text-decoration-line: underline; 90 | } 91 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/api/types/tweet.ts: -------------------------------------------------------------------------------- 1 | import type { TweetEditControl } from "./edit.js"; 2 | import type { Indices, TweetEntities } from "./entities.js"; 3 | import type { MediaDetails } from "./media.js"; 4 | import type { TweetPhoto } from "./photo.js"; 5 | import type { TweetUser } from "./user.js"; 6 | import type { TweetVideo } from "./video.js"; 7 | 8 | /** 9 | * Base tweet information shared by a tweet, a parent tweet and a quoted tweet. 10 | */ 11 | export interface TweetBase { 12 | /** 13 | * Language code of the tweet. E.g "en", "es". 14 | */ 15 | lang: string; 16 | /** 17 | * Creation date of the tweet in the format ISO 8601. 18 | */ 19 | created_at: string; 20 | /** 21 | * Text range of the tweet text. 22 | */ 23 | display_text_range: Indices; 24 | /** 25 | * All the entities that are part of the tweet. Like hashtags, mentions, urls, etc. 26 | */ 27 | entities: TweetEntities; 28 | /** 29 | * The unique identifier of the tweet. 30 | */ 31 | id_str: string; 32 | /** 33 | * The tweet text, including the raw text from the entities. 34 | */ 35 | text: string; 36 | /** 37 | * Information about the user who posted the tweet. 38 | */ 39 | user: TweetUser; 40 | /** 41 | * Edit information about the tweet. 42 | */ 43 | edit_control: TweetEditControl; 44 | isEdited: boolean; 45 | isStaleEdit: boolean; 46 | } 47 | 48 | /** 49 | * A tweet as returned by the the Twitter syndication API. 50 | */ 51 | export interface Tweet extends TweetBase { 52 | __typename: "Tweet"; 53 | favorite_count: number; 54 | mediaDetails?: MediaDetails[]; 55 | photos?: TweetPhoto[]; 56 | video?: TweetVideo; 57 | conversation_count: number; 58 | news_action_type: "conversation"; 59 | quoted_tweet?: QuotedTweet; 60 | in_reply_to_screen_name?: string; 61 | in_reply_to_status_id_str?: string; 62 | in_reply_to_user_id_str?: string; 63 | parent?: TweetParent; 64 | possibly_sensitive?: boolean; 65 | } 66 | 67 | /** 68 | * The parent tweet of a tweet reply. 69 | */ 70 | export interface TweetParent extends TweetBase { 71 | reply_count: number; 72 | retweet_count: number; 73 | favorite_count: number; 74 | } 75 | 76 | /** 77 | * A tweet quoted by another tweet. 78 | */ 79 | export interface QuotedTweet extends TweetBase { 80 | reply_count: number; 81 | retweet_count: number; 82 | favorite_count: number; 83 | mediaDetails?: MediaDetails[]; 84 | self_thread: { 85 | id_str: string; 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/api/get-tweet.ts: -------------------------------------------------------------------------------- 1 | import type { Tweet } from "./types/index.js"; 2 | 3 | const SYNDICATION_URL = "https://cdn.syndication.twimg.com"; 4 | 5 | export class TwitterApiError extends Error { 6 | status: number; 7 | data: any; 8 | 9 | constructor({ 10 | message, 11 | status, 12 | data, 13 | }: { 14 | message: string; 15 | status: number; 16 | data: any; 17 | }) { 18 | super(message); 19 | this.name = "TwitterApiError"; 20 | this.status = status; 21 | this.data = data; 22 | } 23 | } 24 | 25 | const TWEET_ID = /^[0-9]+$/; 26 | 27 | function getToken(id: string) { 28 | return ((Number(id) / 1e15) * Math.PI) 29 | .toString(6 ** 2) 30 | .replace(/(0+|\.)/g, ""); 31 | } 32 | 33 | /** 34 | * Fetches a tweet from the Twitter syndication API. 35 | */ 36 | export async function getTweet( 37 | id: string, 38 | fetchOptions?: RequestInit, 39 | ): Promise { 40 | if (id.length > 40 || !TWEET_ID.test(id)) { 41 | throw new Error(`Invalid tweet id: ${id}`); 42 | } 43 | 44 | const url = new URL(`${SYNDICATION_URL}/tweet-result`); 45 | 46 | url.searchParams.set("id", id); 47 | url.searchParams.set("lang", "en"); 48 | url.searchParams.set( 49 | "features", 50 | [ 51 | "tfw_timeline_list:", 52 | "tfw_follower_count_sunset:true", 53 | "tfw_tweet_edit_backend:on", 54 | "tfw_refsrc_session:on", 55 | "tfw_fosnr_soft_interventions_enabled:on", 56 | "tfw_show_birdwatch_pivots_enabled:on", 57 | "tfw_show_business_verified_badge:on", 58 | "tfw_duplicate_scribes_to_settings:on", 59 | "tfw_use_profile_image_shape_enabled:on", 60 | "tfw_show_blue_verified_badge:on", 61 | "tfw_legacy_timeline_sunset:true", 62 | "tfw_show_gov_verified_badge:on", 63 | "tfw_show_business_affiliate_badge:on", 64 | "tfw_tweet_edit_frontend:on", 65 | ].join(";"), 66 | ); 67 | url.searchParams.set("token", getToken(id)); 68 | 69 | const res = await fetch(url.toString(), fetchOptions); 70 | const isJson = res.headers.get("content-type")?.includes("application/json"); 71 | const data = isJson ? await res.json() : undefined; 72 | 73 | if (res.ok && data.__typename === "TweetTombstone") throw new TwitterApiError({ message: "This tweet is unavailable.", status: res.status, data }); 74 | if (res.ok) return data; 75 | if (res.status === 404) return; 76 | 77 | throw new TwitterApiError({ 78 | message: typeof data.error === "string" ? data.error : "Bad request.", 79 | status: res.status, 80 | data, 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/TweetHeader.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import clsx from "clsx"; 3 | import type { EnrichedTweet } from "../utils.js"; 4 | import type { TwitterComponents } from "./types.jsx"; 5 | import AvatarImg from "./AvatarImg.astro"; 6 | import styles from "./tweet-header.module.css"; 7 | import VerifiedBadge from "./VerifiedBadge.astro"; 8 | 9 | interface Props { 10 | tweet: EnrichedTweet; 11 | components?: TwitterComponents; 12 | } 13 | 14 | const { tweet, components } = Astro.props; 15 | const Img = components?.AvatarImg ?? AvatarImg; 16 | const { user } = tweet; 17 | --- 18 | 19 | 93 | -------------------------------------------------------------------------------- /packages/astro-tweet/src/twitter-theme/TweetMedia.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import clsx from "clsx"; 3 | import { 4 | type EnrichedTweet, 5 | type EnrichedQuotedTweet, 6 | getMediaUrl, 7 | } from "../utils.js"; 8 | import { type MediaDetails } from "../api/index"; 9 | import type { TwitterComponents } from "./types.js"; 10 | // import { TweetMediaVideo } from "./TweetMediaVideo.astro"; 11 | import MediaImg from "./MediaImg.astro"; 12 | import styles from "./tweet-media.module.css"; 13 | import TweetMediaVideo from "./TweetMediaVideo.astro"; 14 | 15 | const getSkeletonStyle = (media: MediaDetails, itemCount: number) => { 16 | let paddingBottom = 56.25; // default of 16x9 17 | 18 | // if we only have 1 item, show at original ratio 19 | if (itemCount === 1) 20 | paddingBottom = 21 | (100 / media.original_info.width) * media.original_info.height; 22 | 23 | // if we have 2 items, double the default to be 16x9 total 24 | if (itemCount === 2) paddingBottom = paddingBottom * 2; 25 | 26 | return { 27 | width: media.type === "photo" ? undefined : "unset", 28 | paddingBottom: `${paddingBottom}%`, 29 | }; 30 | }; 31 | interface Props { 32 | tweet: EnrichedTweet | EnrichedQuotedTweet; 33 | components?: TwitterComponents; 34 | quoted?: boolean; 35 | } 36 | const { tweet, components, quoted } = Astro.props; 37 | const length = tweet.mediaDetails?.length ?? 0; 38 | const Img = components?.MediaImg ?? MediaImg; 39 | --- 40 | 41 |
42 |
1 && styles.grid2Columns, 46 | length === 3 && styles.grid3, 47 | length > 4 && styles.grid2x2, 48 | )} 49 | > 50 | { 51 | tweet.mediaDetails?.map((media: any) => ( 52 | 53 | {media.type === "photo" ? ( 54 | 60 |
64 | {media.ext_alt_text 70 | 71 | ) : ( 72 |
73 |
77 | 78 |
79 | )} 80 | 81 | )) 82 | } 83 |
84 |
85 | -------------------------------------------------------------------------------- /demo/src/layouts/BaseLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Header from "../components/Header.astro"; 3 | const isProd = import.meta.env.PROD; 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 21 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 39 | 40 | 41 | 45 | 46 | 50 | 54 | astro-tweet Demo 55 | 56 | 59 |
60 |
61 | 62 |
63 |
64 |
65 | The source code is available on GitHub 69 |
70 |
71 | Built by Sriram 76 |
77 |
78 | 79 | { 80 | isProd && ( 81 |