diff --git a/.changeset/every-seas-judge.md b/.changeset/every-seas-judge.md
new file mode 100644
index 0000000..b87b080
--- /dev/null
+++ b/.changeset/every-seas-judge.md
@@ -0,0 +1,5 @@
+---
+"@studiocms/socialposter": patch
+---
+
+Initial release
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 1809477..f698a1c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,8 +4,11 @@
"alignjustify",
"alignleft",
"alignright",
+ "astroenv",
+ "atproto",
"Avenir",
"backcolor",
+ "bsky",
"bullist",
"cmdm",
"CMSWYSIWYG",
@@ -19,10 +22,12 @@
"numlist",
"outdent",
"projectdata",
+ "socialposter",
"strikethrough",
"studiocms",
"studiosdk",
"toastr",
+ "virtuals",
"withstudiocms",
"WYSIWYGDB"
]
diff --git a/packages/studiocms_socialposter/.gitignore b/packages/studiocms_socialposter/.gitignore
new file mode 100644
index 0000000..8f18003
--- /dev/null
+++ b/packages/studiocms_socialposter/.gitignore
@@ -0,0 +1,23 @@
+
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+.npmrc
+
+dist/
\ No newline at end of file
diff --git a/packages/studiocms_socialposter/LICENSE b/packages/studiocms_socialposter/LICENSE
new file mode 100644
index 0000000..443a97b
--- /dev/null
+++ b/packages/studiocms_socialposter/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 StudioCMS - Adam Matthiesen, Jacob Jenkins, Paul Valladares
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/packages/studiocms_socialposter/README.md b/packages/studiocms_socialposter/README.md
new file mode 100644
index 0000000..51bf069
--- /dev/null
+++ b/packages/studiocms_socialposter/README.md
@@ -0,0 +1,48 @@
+# @StudioCMS/socialposter Plugin
+
+Allow cross-posting to your social media accounts from your StudioCMS dashboard with ease!
+
+## Requirements
+
+Depending on which services you are trying to post on you may require one of more of the following variables in your `.env`
+
+```bash
+# Twitter/X
+TWITTER_API_KEY=your_api_key
+TWITTER_API_SECRET=your_api_secret
+TWITTER_ACCESS_TOKEN=your_access_token
+TWITTER_ACCESS_SECRET=your_access_secret
+
+# Bluesky
+BLUESKY_SERVICE=https://bsky.social
+BLUESKY_USERNAME=your_username
+BLUESKY_PASSWORD=your_password
+
+# Threads/Instagram
+THREADS_USER_ID=your_user_id
+THREADS_ACCESS_TOKEN=your_access_token
+```
+
+### Usage
+
+Add this plugin in your StudioCMS config (`studiocms.config.mjs`) and enable the desired options.
+
+```ts
+import { defineStudioCMSConfig } from 'studiocms/config';
+import socialPoster from '@studiocms/socialposter';
+
+export default defineStudioCMSConfig({
+ // other options here
+ plugins: [
+ socialPoster({
+ bluesky: false,
+ threads: false,
+ twitter: false,
+ })
+ ],
+});
+```
+
+## License
+
+[MIT Licensed](./LICENSE).
\ No newline at end of file
diff --git a/packages/studiocms_socialposter/astroenv.d.ts b/packages/studiocms_socialposter/astroenv.d.ts
new file mode 100644
index 0000000..80e50f8
--- /dev/null
+++ b/packages/studiocms_socialposter/astroenv.d.ts
@@ -0,0 +1,11 @@
+declare module 'astro:env/server' {
+ export const TWITTER_API_KEY: string | undefined;
+ export const TWITTER_API_SECRET: string | undefined;
+ export const TWITTER_ACCESS_TOKEN: string | undefined;
+ export const TWITTER_ACCESS_SECRET: string | undefined;
+ export const BLUESKY_SERVICE: string | undefined;
+ export const BLUESKY_USERNAME: string | undefined;
+ export const BLUESKY_PASSWORD: string | undefined;
+ export const THREADS_USER_ID: string | undefined;
+ export const THREADS_ACCESS_TOKEN: string | undefined;
+}
diff --git a/packages/studiocms_socialposter/package.json b/packages/studiocms_socialposter/package.json
new file mode 100644
index 0000000..1d8a706
--- /dev/null
+++ b/packages/studiocms_socialposter/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@studiocms/socialposter",
+ "version": "0.1.0-experimental.0",
+ "description": "Allow cross-posting to your social media accounts from your StudioCMS dashboard with ease!",
+ "author": {
+ "name": "Adam Matthiesen | Jacob Jenkins | Paul Valladares",
+ "url": "https://studiocms.dev"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/withstudiocms/experiments.git",
+ "directory": "packages/studiocms_socialposter"
+ },
+ "contributors": ["Adammatthiesen", "jdtjenkins", "dreyfus92", "code.spirit"],
+ "license": "MIT",
+ "keywords": [
+ "astro",
+ "astrocms",
+ "astrodb",
+ "astrostudio",
+ "astro-integration",
+ "astro-studio",
+ "astro-studiocms",
+ "cms",
+ "studiocms",
+ "withastro",
+ "plugin",
+ "studiocms-plugin"
+ ],
+ "homepage": "https://studiocms.dev",
+ "publishConfig": {
+ "access": "public",
+ "provenance": true
+ },
+ "sideEffects": false,
+ "files": ["dist"],
+ "scripts": {
+ "build": "build-scripts build 'src/**/*.{ts,astro,css,js}'",
+ "dev": "build-scripts dev 'src/**/*.{ts,astro,css,js}'"
+ },
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ }
+ },
+ "type": "module",
+ "dependencies": {
+ "@atproto/api": "^0.14.20",
+ "@studiocms/ui": "^0.4.16",
+ "astro-integration-kit": "catalog:",
+ "twitter-api-v2": "^1.22.0",
+ "quill": "^2.0.3"
+ },
+ "devDependencies": {
+ "@types/node": "catalog:"
+ },
+ "peerDependencies": {
+ "astro": "catalog:min",
+ "studiocms": "catalog:min",
+ "vite": "catalog:min"
+ }
+}
diff --git a/packages/studiocms_socialposter/src/index.ts b/packages/studiocms_socialposter/src/index.ts
new file mode 100644
index 0000000..eb58e83
--- /dev/null
+++ b/packages/studiocms_socialposter/src/index.ts
@@ -0,0 +1,151 @@
+/**
+ * These triple-slash directives defines dependencies to various declaration files that will be
+ * loaded when a user imports the StudioCMS plugin in their Astro configuration file. These
+ * directives must be first at the top of the file and can only be preceded by this comment.
+ */
+///
+///
+///
+///
+import { addVirtualImports, createResolver } from 'astro-integration-kit';
+import { type StudioCMSPlugin, definePlugin } from 'studiocms/plugins';
+import { addAstroEnvConfig } from './utils/astroEnvConfig.js';
+import { envField } from 'astro/config';
+
+export interface StudioCMSSocialPosterOptions {
+ bluesky: boolean;
+ threads: boolean;
+ twitter: boolean;
+}
+
+const defaultOptions: StudioCMSSocialPosterOptions = {
+ bluesky: false,
+ threads: false,
+ twitter: false,
+};
+
+function studiocmsSocialPoster(opts?: Partial): StudioCMSPlugin {
+ // Resolve the path to the current file
+ const { resolve } = createResolver(import.meta.url);
+
+ // Define the package identifier
+ const packageIdentifier = '@studiocms/socialposter';
+
+ const options: StudioCMSSocialPosterOptions = {
+ ...defaultOptions,
+ ...opts,
+ };
+
+ return definePlugin({
+ identifier: packageIdentifier,
+ name: 'StudioCMS Social Poster',
+ studiocmsMinimumVersion: '0.1.0-beta.14',
+ dashboardPages: {
+ user: [
+ {
+ title: {
+ 'en-us': 'Share to Social Media',
+ },
+ description: 'Share content on Social media',
+ sidebar: 'single',
+ pageBodyComponent: resolve('./pages/socials.astro'),
+ route: 'share',
+ requiredPermissions: 'editor',
+ icon: 'share',
+ },
+ ],
+ },
+ integration: {
+ name: packageIdentifier,
+ hooks: {
+ 'astro:config:setup': (params) => {
+ const { injectRoute } = params;
+
+ addAstroEnvConfig(params, {
+ validateSecrets: true,
+ schema: {
+ BLUESKY_SERVICE: envField.string({
+ context: 'server',
+ access: 'secret',
+ optional: !options.bluesky,
+ }),
+ BLUESKY_USERNAME: envField.string({
+ context: 'server',
+ access: 'secret',
+ optional: !options.bluesky,
+ }),
+ BLUESKY_PASSWORD: envField.string({
+ context: 'server',
+ access: 'secret',
+ optional: !options.bluesky,
+ }),
+ THREADS_USER_ID: envField.string({
+ context: 'server',
+ access: 'secret',
+ optional: !options.threads,
+ }),
+ THREADS_ACCESS_TOKEN: envField.string({
+ context: 'server',
+ access: 'secret',
+ optional: !options.threads,
+ }),
+ TWITTER_API_KEY: envField.string({
+ context: 'server',
+ access: 'secret',
+ optional: !options.twitter,
+ }),
+ TWITTER_API_SECRET: envField.string({
+ context: 'server',
+ access: 'secret',
+ optional: !options.twitter,
+ }),
+ TWITTER_ACCESS_TOKEN: envField.string({
+ context: 'server',
+ access: 'secret',
+ optional: !options.twitter,
+ }),
+ TWITTER_ACCESS_SECRET: envField.string({
+ context: 'server',
+ access: 'secret',
+ optional: !options.twitter,
+ }),
+ },
+ });
+
+ addVirtualImports(params, {
+ name: packageIdentifier,
+ imports: {
+ 'studiocms:socialposter/config': `export default ${JSON.stringify(options)}`,
+ },
+ });
+
+ if (options.bluesky) {
+ injectRoute({
+ pattern: '/studiocms_api/socialposter/post-to-bluesky',
+ entrypoint: resolve('./routes/postToBlueSky.js'),
+ prerender: false,
+ });
+ }
+
+ if (options.threads) {
+ injectRoute({
+ pattern: '/studiocms_api/socialposter/post-to-threads',
+ entrypoint: resolve('./routes/postToThreads.js'),
+ prerender: false,
+ });
+ }
+
+ if (options.twitter) {
+ injectRoute({
+ pattern: '/studiocms_api/socialposter/post-to-twitter',
+ entrypoint: resolve('./routes/postToTwitter.js'),
+ prerender: false,
+ });
+ }
+ },
+ },
+ },
+ });
+}
+
+export default studiocmsSocialPoster;
diff --git a/packages/studiocms_socialposter/src/pages/socials.astro b/packages/studiocms_socialposter/src/pages/socials.astro
new file mode 100644
index 0000000..fba9043
--- /dev/null
+++ b/packages/studiocms_socialposter/src/pages/socials.astro
@@ -0,0 +1,413 @@
+---
+import 'quill/dist/quill.snow.css';
+import socialsConfig from 'studiocms:socialposter/config';
+import { Card, Toggle, Button, Icon } from 'studiocms:ui/components';
+
+const { bluesky, threads, twitter } = socialsConfig;
+
+const options = [
+ {
+ label: 'BlueSky',
+ name: 'bluesky',
+ counterId: 'bsky-counter',
+ limit: 300,
+ enabled: bluesky,
+ },
+ {
+ label: 'Threads / Instagram',
+ name: 'threads',
+ counterId: 'threads-counter',
+ limit: 500,
+ enabled: threads,
+ },
+ {
+ label: 'Twitter / X',
+ name: 'twitter',
+ counterId: 'twitter-counter',
+ limit: 280,
+ enabled: twitter,
+ },
+];
+---
+
+
+ {/*
+
+ // TODO: Add support for being able to display pages and their current content to make it easy to get excerpts for creating new posts.
+
+ */}
+
+
+