├── .ddev ├── commands │ └── web │ │ └── fe ├── config.yaml └── nginx_full │ ├── api-site.conf │ └── next-site.conf ├── .gitignore ├── LICENCE ├── README.md ├── backend ├── .env.example ├── .gitignore ├── LICENSE ├── bootstrap.php ├── composer.json ├── composer.lock ├── config │ ├── app.php │ ├── app.web.php │ ├── general.php │ ├── htmlpurifier │ │ └── Default.json │ ├── project │ │ ├── ckeditor │ │ │ └── configs │ │ │ │ ├── 7dac7ab3-08a0-4af8-ba7c-cbecd0726f08.yaml │ │ │ │ └── b579b454-12bd-4710-a436-c26907141957.yaml │ │ ├── entryTypes │ │ │ ├── callout--f7576aa3-c155-4b54-82c9-8cb8c93495c9.yaml │ │ │ ├── category--5dd08fc8-3ea4-4582-952f-4c0f7ebf409e.yaml │ │ │ ├── global--d55d172f-8b8f-4bb7-b680-31249981c350.yaml │ │ │ ├── image--5358db38-27c0-49b4-b9d3-e68326e79da2.yaml │ │ │ ├── page--697da4c2-e02f-4f49-9d76-e22609ceda25.yaml │ │ │ ├── text--25b2446d-c1d2-4aa1-8184-3147e617f069.yaml │ │ │ └── video--a06e695e-af7f-4a1b-84a7-575ec935e293.yaml │ │ ├── fields │ │ │ ├── address--163c895f-0c05-4845-80e2-043b2346d830.yaml │ │ │ ├── category--d390347a-09cf-4b70-a3b4-bc3cca520623.yaml │ │ │ ├── code--9d082133-4710-4468-ab39-80db8c9fcf59.yaml │ │ │ ├── dividerType--6c819fc7-3f73-4873-8f0c-5ea322fef1f9.yaml │ │ │ ├── image--6c9778b8-507c-4108-937d-6ceeabf35047.yaml │ │ │ ├── navigation--2bae9725-9546-4f55-a889-56102191aa89.yaml │ │ │ ├── page--d4f0917b-edcf-4ef0-a98b-661ff27384ad.yaml │ │ │ ├── pageContent--0a58e287-e038-4eb7-ab2a-cf4cbdbe013d.yaml │ │ │ ├── text--6d2e5577-5ded-43c9-96e2-d4fc4c770fe9.yaml │ │ │ └── textBlock--9d42aa56-4a66-4d0b-b8a9-84a4cb387788.yaml │ │ ├── graphql │ │ │ ├── graphql.yaml │ │ │ └── schemas │ │ │ │ ├── 2b829e15-a7bd-4ab0-9050-dcdfe44499cf.yaml │ │ │ │ └── c7d2eb61-cdde-4a76-88a9-eb30ddcf155b.yaml │ │ ├── imageTransforms │ │ │ ├── avatar--b5b7bfc5-a39e-4d1a-89be-51f0afeb5ce0.yaml │ │ │ ├── hero--0413e9dc-1c8a-4221-b81d-c9f6c0594cbd.yaml │ │ │ └── wide--0922209d-2911-419e-82cf-b05113a35f6e.yaml │ │ ├── project.yaml │ │ ├── sections │ │ │ ├── blog--998bf681-e007-4f7e-a525-863e01a38560.yaml │ │ │ ├── blogCategories--70b2614b-639a-4de2-8c11-3851d36d6f8c.yaml │ │ │ ├── blogPosts--6ba54824-1c3f-454f-8830-f6cea379bee4.yaml │ │ │ ├── global--bd99645a-4524-40e4-83dc-c6b12e023bab.yaml │ │ │ ├── guestbook--aef20f9c-5c84-4c50-8a18-acf0452db62f.yaml │ │ │ ├── guestbookPosts--3e2a1a6f-88ea-477b-936d-39393f2d594f.yaml │ │ │ ├── home--cdad2202-5d55-49ba-995e-962503f5887f.yaml │ │ │ └── pages--c4b4f50d-7d62-4a4c-bbf8-7aae00fd82c7.yaml │ │ ├── siteGroups │ │ │ └── 6d0d3aa4-84be-441d-8fcc-2ab2d6db2503.yaml │ │ ├── sites │ │ │ └── default--58ccb571-6c72-449f-8e7b-6f43c5d416b3.yaml │ │ ├── users │ │ │ ├── fieldLayouts │ │ │ │ └── 7e6be4ec-feec-4e60-91d5-3669ac76ea98.yaml │ │ │ └── users.yaml │ │ └── volumes │ │ │ └── media--7b9364bd-2215-495a-8b0b-2280df887a8b.yaml │ └── routes.php ├── craft ├── craft-cloud.yaml ├── modules │ └── authorinfo │ │ └── Module.php ├── storage │ ├── .gitignore │ └── config-deltas │ │ └── .gitignore ├── templates │ ├── .gitignore │ ├── _partials │ │ └── entry │ │ │ ├── callout.twig │ │ │ ├── image.twig │ │ │ ├── text.twig │ │ │ └── video.twig │ └── _utilities │ │ └── image.twig └── web │ ├── .htaccess │ ├── cpresources │ └── .gitignore │ ├── index.php │ └── web.config └── frontend ├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── jsconfig.json ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg └── src ├── app ├── [...slug] │ └── page.jsx ├── blog │ ├── [slug] │ │ └── page.jsx │ └── page.jsx ├── favicon.ico ├── globals.css ├── guestbook │ └── page.jsx ├── layout.jsx ├── loading.jsx ├── not-found.jsx ├── page.jsx └── safelist.txt ├── components ├── Alert.jsx ├── BlogList.jsx ├── Content.jsx ├── Footer.jsx ├── GuestbookInteractive.jsx ├── Header.jsx ├── Image.jsx ├── Logo.jsx ├── Navigation.jsx ├── Pagination.jsx ├── PostForm.jsx ├── PostList.jsx ├── Preview.jsx ├── SkipLink.jsx └── Teaser.jsx ├── lib ├── createPage.js ├── flashes.js └── graphql.js └── queries ├── blog.mjs ├── blogPosts.mjs ├── globals.js ├── guestbook.mjs ├── guestbookPosts.mjs ├── home.js ├── pages.mjs └── post.mjs /.ddev/commands/web/fe: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd ./frontend && "$@" 4 | -------------------------------------------------------------------------------- /.ddev/config.yaml: -------------------------------------------------------------------------------- 1 | name: starter-next 2 | type: craftcms 3 | docroot: backend/web 4 | php_version: "8.2" 5 | nodejs_version: "20" 6 | webserver_type: nginx-fpm 7 | composer_root: backend 8 | xdebug_enabled: false 9 | additional_hostnames: 10 | - api.starter-next 11 | database: 12 | type: mysql 13 | version: "8.0" 14 | use_dns_when_possible: true 15 | composer_version: "2" 16 | corepack_enable: false 17 | upload_dirs: 18 | - uploads 19 | web_extra_exposed_ports: 20 | - name: next 21 | container_port: 3000 22 | http_port: 3000 23 | https_port: 3001 24 | web_environment: 25 | - VIRTUAL_HOST=starter-next.ddev.site,api.starter-next.ddev.site 26 | - CRAFT_CMD_ROOT=/var/www/html/backend 27 | 28 | # Key features of DDEV's config.yaml: 29 | 30 | # name: # Name of the project, automatically provides 31 | # http://projectname.ddev.site and https://projectname.ddev.site 32 | 33 | # type: # backdrop, craftcms, django4, drupal, drupal6, drupal7, laravel, magento, magento2, php, python, shopware6, silverstripe, typo3, wordpress 34 | # See https://ddev.readthedocs.io/en/stable/users/quickstart/ for more 35 | # information on the different project types 36 | # "drupal" covers recent Drupal 8+ 37 | 38 | # docroot: # Relative path to the directory containing index.php. 39 | 40 | # php_version: "8.2" # PHP version to use, "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4" 41 | 42 | # You can explicitly specify the webimage but this 43 | # is not recommended, as the images are often closely tied to DDEV's' behavior, 44 | # so this can break upgrades. 45 | 46 | # webimage: # nginx/php docker image. 47 | 48 | # database: 49 | # type: # mysql, mariadb, postgres 50 | # version: # database version, like "10.11" or "8.0" 51 | # MariaDB versions can be 5.5-10.8, 10.11, and 11.4. 52 | # MySQL versions can be 5.5-8.0. 53 | # PostgreSQL versions can be 9-16. 54 | 55 | # router_http_port: # Port to be used for http (defaults to global configuration, usually 80) 56 | # router_https_port: # Port for https (defaults to global configuration, usually 443) 57 | 58 | # xdebug_enabled: false # Set to true to enable Xdebug and "ddev start" or "ddev restart" 59 | # Note that for most people the commands 60 | # "ddev xdebug" to enable Xdebug and "ddev xdebug off" to disable it work better, 61 | # as leaving Xdebug enabled all the time is a big performance hit. 62 | 63 | # xhprof_enabled: false # Set to true to enable Xhprof and "ddev start" or "ddev restart" 64 | # Note that for most people the commands 65 | # "ddev xhprof" to enable Xhprof and "ddev xhprof off" to disable it work better, 66 | # as leaving Xhprof enabled all the time is a big performance hit. 67 | 68 | # webserver_type: nginx-fpm, apache-fpm, or nginx-gunicorn 69 | 70 | # timezone: Europe/Berlin 71 | # This is the timezone used in the containers and by PHP; 72 | # it can be set to any valid timezone, 73 | # see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones 74 | # For example Europe/Dublin or MST7MDT 75 | 76 | # composer_root: 77 | # Relative path to the Composer root directory from the project root. This is 78 | # the directory which contains the composer.json and where all Composer related 79 | # commands are executed. 80 | 81 | # composer_version: "2" 82 | # You can set it to "" or "2" (default) for Composer v2 or "1" for Composer v1 83 | # to use the latest major version available at the time your container is built. 84 | # It is also possible to use each other Composer version channel. This includes: 85 | # - 2.2 (latest Composer LTS version) 86 | # - stable 87 | # - preview 88 | # - snapshot 89 | # Alternatively, an explicit Composer version may be specified, for example "2.2.18". 90 | # To reinstall Composer after the image was built, run "ddev debug refresh". 91 | 92 | # nodejs_version: "20" 93 | # change from the default system Node.js version to any other version. 94 | # See https://ddev.readthedocs.io/en/stable/users/configuration/config/#nodejs_version for more information 95 | # and https://www.npmjs.com/package/n#specifying-nodejs-versions for the full documentation, 96 | # Note that using of 'ddev nvm' is discouraged because "nodejs_version" is much easier to use, 97 | # can specify any version, and is more robust than using 'nvm'. 98 | 99 | # corepack_enable: false 100 | # Change to 'true' to 'corepack enable' and gain access to latest versions of yarn/pnpm 101 | 102 | # additional_hostnames: 103 | # - somename 104 | # - someothername 105 | # would provide http and https URLs for "somename.ddev.site" 106 | # and "someothername.ddev.site". 107 | 108 | # additional_fqdns: 109 | # - example.com 110 | # - sub1.example.com 111 | # would provide http and https URLs for "example.com" and "sub1.example.com" 112 | # Please take care with this because it can cause great confusion. 113 | 114 | # upload_dirs: "custom/upload/dir" 115 | # 116 | # upload_dirs: 117 | # - custom/upload/dir 118 | # - ../private 119 | # 120 | # would set the destination paths for ddev import-files to /custom/upload/dir 121 | # When Mutagen is enabled this path is bind-mounted so that all the files 122 | # in the upload_dirs don't have to be synced into Mutagen. 123 | 124 | # disable_upload_dirs_warning: false 125 | # If true, turns off the normal warning that says 126 | # "You have Mutagen enabled and your 'php' project type doesn't have upload_dirs set" 127 | 128 | # ddev_version_constraint: "" 129 | # Example: 130 | # ddev_version_constraint: ">= 1.22.4" 131 | # This will enforce that the running ddev version is within this constraint. 132 | # See https://github.com/Masterminds/semver#checking-version-constraints for 133 | # supported constraint formats 134 | 135 | # working_dir: 136 | # web: /var/www/html 137 | # db: /home 138 | # would set the default working directory for the web and db services. 139 | # These values specify the destination directory for ddev ssh and the 140 | # directory in which commands passed into ddev exec are run. 141 | 142 | # omit_containers: [db, ddev-ssh-agent] 143 | # Currently only these containers are supported. Some containers can also be 144 | # omitted globally in the ~/.ddev/global_config.yaml. Note that if you omit 145 | # the "db" container, several standard features of DDEV that access the 146 | # database container will be unusable. In the global configuration it is also 147 | # possible to omit ddev-router, but not here. 148 | 149 | # performance_mode: "global" 150 | # DDEV offers performance optimization strategies to improve the filesystem 151 | # performance depending on your host system. Should be configured globally. 152 | # 153 | # If set, will override the global config. Possible values are: 154 | # - "global": uses the value from the global config. 155 | # - "none": disables performance optimization for this project. 156 | # - "mutagen": enables Mutagen for this project. 157 | # - "nfs": enables NFS for this project. 158 | # 159 | # See https://ddev.readthedocs.io/en/stable/users/install/performance/#nfs 160 | # See https://ddev.readthedocs.io/en/stable/users/install/performance/#mutagen 161 | 162 | # fail_on_hook_fail: False 163 | # Decide whether 'ddev start' should be interrupted by a failing hook 164 | 165 | # host_https_port: "59002" 166 | # The host port binding for https can be explicitly specified. It is 167 | # dynamic unless otherwise specified. 168 | # This is not used by most people, most people use the *router* instead 169 | # of the localhost port. 170 | 171 | # host_webserver_port: "59001" 172 | # The host port binding for the ddev-webserver can be explicitly specified. It is 173 | # dynamic unless otherwise specified. 174 | # This is not used by most people, most people use the *router* instead 175 | # of the localhost port. 176 | 177 | # host_db_port: "59002" 178 | # The host port binding for the ddev-dbserver can be explicitly specified. It is dynamic 179 | # unless explicitly specified. 180 | 181 | # mailpit_http_port: "8025" 182 | # mailpit_https_port: "8026" 183 | # The Mailpit ports can be changed from the default 8025 and 8026 184 | 185 | # host_mailpit_port: "8025" 186 | # The mailpit port is not normally bound on the host at all, instead being routed 187 | # through ddev-router, but it can be bound directly to localhost if specified here. 188 | 189 | # webimage_extra_packages: [php7.4-tidy, php-bcmath] 190 | # Extra Debian packages that are needed in the webimage can be added here 191 | 192 | # dbimage_extra_packages: [telnet,netcat] 193 | # Extra Debian packages that are needed in the dbimage can be added here 194 | 195 | # use_dns_when_possible: true 196 | # If the host has internet access and the domain configured can 197 | # successfully be looked up, DNS will be used for hostname resolution 198 | # instead of editing /etc/hosts 199 | # Defaults to true 200 | 201 | # project_tld: ddev.site 202 | # The top-level domain used for project URLs 203 | # The default "ddev.site" allows DNS lookup via a wildcard 204 | # If you prefer you can change this to "ddev.local" to preserve 205 | # pre-v1.9 behavior. 206 | 207 | # ngrok_args: --basic-auth username:pass1234 208 | # Provide extra flags to the "ngrok http" command, see 209 | # https://ngrok.com/docs/ngrok-agent/config or run "ngrok http -h" 210 | 211 | # disable_settings_management: false 212 | # If true, DDEV will not create CMS-specific settings files like 213 | # Drupal's settings.php/settings.ddev.php or TYPO3's additional.php 214 | # In this case the user must provide all such settings. 215 | 216 | # You can inject environment variables into the web container with: 217 | # web_environment: 218 | # - SOMEENV=somevalue 219 | # - SOMEOTHERENV=someothervalue 220 | 221 | # no_project_mount: false 222 | # (Experimental) If true, DDEV will not mount the project into the web container; 223 | # the user is responsible for mounting it manually or via a script. 224 | # This is to enable experimentation with alternate file mounting strategies. 225 | # For advanced users only! 226 | 227 | # bind_all_interfaces: false 228 | # If true, host ports will be bound on all network interfaces, 229 | # not the localhost interface only. This means that ports 230 | # will be available on the local network if the host firewall 231 | # allows it. 232 | 233 | # default_container_timeout: 120 234 | # The default time that DDEV waits for all containers to become ready can be increased from 235 | # the default 120. This helps in importing huge databases, for example. 236 | 237 | #web_extra_exposed_ports: 238 | #- name: nodejs 239 | # container_port: 3000 240 | # http_port: 2999 241 | # https_port: 3000 242 | #- name: something 243 | # container_port: 4000 244 | # https_port: 4000 245 | # http_port: 3999 246 | # Allows a set of extra ports to be exposed via ddev-router 247 | # Fill in all three fields even if you don’t intend to use the https_port! 248 | # If you don’t add https_port, then it defaults to 0 and ddev-router will fail to start. 249 | # 250 | # The port behavior on the ddev-webserver must be arranged separately, for example 251 | # using web_extra_daemons. 252 | # For example, with a web app on port 3000 inside the container, this config would 253 | # expose that web app on https://.ddev.site:9999 and http://.ddev.site:9998 254 | # web_extra_exposed_ports: 255 | # - name: myapp 256 | # container_port: 3000 257 | # http_port: 9998 258 | # https_port: 9999 259 | 260 | #web_extra_daemons: 261 | #- name: "http-1" 262 | # command: "/var/www/html/node_modules/.bin/http-server -p 3000" 263 | # directory: /var/www/html 264 | #- name: "http-2" 265 | # command: "/var/www/html/node_modules/.bin/http-server /var/www/html/sub -p 3000" 266 | # directory: /var/www/html 267 | 268 | # override_config: false 269 | # By default, config.*.yaml files are *merged* into the configuration 270 | # But this means that some things can't be overridden 271 | # For example, if you have 'use_dns_when_possible: true'' you can't override it with a merge 272 | # and you can't erase existing hooks or all environment variables. 273 | # However, with "override_config: true" in a particular config.*.yaml file, 274 | # 'use_dns_when_possible: false' can override the existing values, and 275 | # hooks: 276 | # post-start: [] 277 | # or 278 | # web_environment: [] 279 | # or 280 | # additional_hostnames: [] 281 | # can have their intended affect. 'override_config' affects only behavior of the 282 | # config.*.yaml file it exists in. 283 | 284 | # Many DDEV commands can be extended to run tasks before or after the 285 | # DDEV command is executed, for example "post-start", "post-import-db", 286 | # "pre-composer", "post-composer" 287 | # See https://ddev.readthedocs.io/en/stable/users/extend/custom-commands/ for more 288 | # information on the commands that can be extended and the tasks you can define 289 | # for them. Example: 290 | #hooks: 291 | -------------------------------------------------------------------------------- /.ddev/nginx_full/api-site.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen 443 ssl; 4 | 5 | # Update this if you want to change the project’s back-end URL: 6 | server_name api.starter-next.ddev.site; 7 | 8 | root /var/www/html/backend/web; 9 | index index.php index.html; 10 | 11 | location / { 12 | try_files $uri $uri/ /index.php$is_args$args; 13 | } 14 | 15 | location ~ \.php$ { 16 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 17 | fastcgi_pass unix:/run/php-fpm.sock; 18 | fastcgi_index index.php; 19 | include fastcgi_params; 20 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 21 | fastcgi_param PATH_INFO $fastcgi_path_info; 22 | } 23 | 24 | ssl_certificate /etc/ssl/certs/master.crt; 25 | ssl_certificate_key /etc/ssl/certs/master.key; 26 | 27 | include /etc/nginx/monitoring.conf; 28 | } 29 | -------------------------------------------------------------------------------- /.ddev/nginx_full/next-site.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen 443 ssl; 4 | 5 | # Update this if you want to change the project’s front-end URL: 6 | server_name starter-next.ddev.site; 7 | 8 | location / { 9 | proxy_pass http://localhost:3000; 10 | proxy_http_version 1.1; 11 | proxy_set_header Upgrade $http_upgrade; 12 | proxy_set_header Connection 'upgrade'; 13 | proxy_set_header Host $host; 14 | proxy_cache_bypass $http_upgrade; 15 | } 16 | 17 | ssl_certificate /etc/ssl/certs/master.crt; 18 | ssl_certificate_key /etc/ssl/certs/master.key; 19 | 20 | include /etc/nginx/monitoring.conf; 21 | 22 | error_log /dev/stdout info; 23 | access_log /var/log/nginx/access.log; 24 | 25 | include /etc/nginx/common.d/*.conf; 26 | include /mnt/ddev_config/nginx/*.conf; 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Craft CMS + Next.js Starter 2 | 3 | A minimal, production-ready starter for [Next.js 15](https://nextjs.org) and [Craft CMS](https://craftcms.com/) projects. Check out the [features](#key-features), or [dive right in](#quick-start)! 4 | 5 | > [!TIP] 6 | > Curious about Craft, but want to try it with a different framework? We have [other starter projects](https://craftcms.com/starters), too! 7 | 8 | ## Quick Start 9 | 10 | This project assumes you have our recommended development environment [DDEV](https://ddev.com) installed and up-to-date. 11 | 12 | 1. Clone this repository, and move into the new directory: 13 | 14 | ```bash 15 | git clone https://github.com/craftcms/starter-next.git 16 | # ... 17 | cd starter-next 18 | ``` 19 | 20 | 1. (Optional) Adjust the DDEV project name and domains. See [this section](#running-on-a-different-domain) for more information. 21 | 22 | 1. Set up the Craft CMS backend: 23 | 24 | ```bash 25 | ddev composer install 26 | ddev craft install 27 | ``` 28 | 29 | Write down the username and password you choose, during installation. You’ll need it to [log in to the control panel](#control-panel). 30 | 31 | 1. Generate a token for the _Guestbook_ GraphQL schema: 32 | 33 | ```bash 34 | # Display a list of schemas and UUIDs: 35 | ddev craft graphql/list-schemas 36 | 37 | # Use the “Guestbook” schema ID to generate a token: 38 | ddev craft graphql/create-token c7d2eb61-cdde-4a76-88a9-eb30ddcf155b 39 | ``` 40 | 41 | (The _Guestbook_ schema is automatically created with the appropriate permissions via [Project Config](https://craftcms.com/docs/5.x/system/project-config.html) as Craft is installed, but _tokens_ are unique to each environment.) 42 | 43 | 1. Add required Next.js configuration: 44 | 45 | - Copy `frontend/.env.example` to `frontend/.env`; 46 | - Populate the `GRAPHQL_TOKEN` variable with the token you just generated; 47 | 48 | 1. Install front-end dependencies and start the Next.js development server: 49 | 50 | ```bash 51 | ddev fe npm install 52 | ddev fe npm run dev 53 | ``` 54 | 55 | > [!TIP] 56 | > The URLs that Next.js emits as it boots up may not work—they are correct _inside_ their respective containers, but must be accessed from the outside via the pre-configured DDEV hostnames. 57 | 58 | ### Post-Install 59 | 60 | Run `ddev launch` to open the front end in your default browser, or visit `https://starter-next.ddev.site`. 61 | 62 | ### Control Panel 63 | 64 | The Craft [control panel](https://craftcms.com/docs/5.x/system/control-panel.html) is available at `https://api.starter-next.ddev.site/admin`. Log in with the username and password you created during installation! 65 | 66 | ## Key Features 67 | 68 | This project includes basic support for a handful of Craft’s best features, in a tidy headless package built on [Next.js 15](https://nextjs.org). 69 | 70 | ### GraphQL 71 | 72 | Next.js communicates with Craft’s built-in [GraphQL API](https://craftcms.com/docs/5.x/development/graphql.html) to query posts and pages, and create (or “mutate”) guestbook entries. 73 | 74 | ### Live Preview 75 | 76 | Craft’s live preview works just as you’d expect. You can even copy a secure, sharable URL to any draft. 77 | 78 | ### Pagination 79 | 80 | The blog, category feeds, and the guestbook are neatly paginated in a way that matches Craft’s native handling. Progress through a set of paginated results is reflected in the URL and your browser’s navigation history. 81 | 82 | ## Project Structure 83 | 84 | We’ve split the project directory into two folders, `backend/` and `frontend/`, to better demonstrate the boundaries of Craft and Next.js, respectively. Some configuration needs to be transcribed between the spaces to ensure each half understands where the other lives! 85 | 86 | > [!WARNING] 87 | > The front- and back-end `.env` files are separate! Make sure you are updating configuration in the correct file. 88 | 89 | There is no `.gitignore` at the root of the project—instead, each system maintains its own relatively-vanilla file (`backend/.gitignore` and `frontend/.gitignore`). 90 | 91 | ### Back End 92 | 93 | The `backend/` directory is predominantly a standard Craft installation, so [its structure](https://craftcms.com/docs/5.x/system/directory-structure.html) should be familiar. Craft is configured to run in [headless mode](https://craftcms.com/docs/5.x/reference/config/general.html#headlessmode), which means it doesn’t perform any element routing, nor template rendering—in fact, it will only respond to _control panel_, _action_, and static asset requests (like any images you might upload). 94 | 95 | Craft uses the `PRIMARY_SITE_URL` environment variable (automatically set by DDEV) to generate fully-qualified URLs for front-end pages, and the `CRAFT_BASE_CP_URL` (predefined in `backend/.env`) to build control panel and asset URLs. 96 | 97 | ### Front End 98 | 99 | Next.js lives in the `frontend/` directory. All NPM commands should be executed here—as a convenience, we’ve included a custom DDEV command (`.ddev/commands/web/fe`) that ensures tasks are run in the appropriate directory: 100 | 101 | - `ddev fe npm install` → Moves into `frontend/`, then executes `npm install`; 102 | - `ddev fe npm run dev` → Moves into `frontend/`, then executes the user-defined `dev` script; 103 | 104 | See `frontend/next.config.js` to [customize Next.js](https://nextjs.org/docs/app/api-reference/next-config-js), or read about the rest of its [project structure](https://nextjs.org/docs/getting-started/project-structure). 105 | 106 | Routing is handled primarily via the [`app/`](https://nextjs.org/docs/app/building-your-application/routing) directory, and GraphQL queries are centralized in `queries/`. 107 | 108 | ## Tips + Tricks 109 | 110 | ### GraphQL Playground 111 | 112 | If you want to compress the GraphQL query feedback loop, open up the Craft control panel and click **GraphQL** in the main navigation, then choose **GraphiQL**. [Read more about the GraphQL IDE](https://craftcms.com/docs/5.x/development/graphql.html#using-the-graphiql-ide) in the Craft documentation. 113 | 114 | ### Running on a Different Domain 115 | 116 | The DDEV configuration files shipped with this project use a specific pair of URLs for the Next.js front end and Craft back end: 117 | 118 | - **Front end**: `https://starter-next.ddev.site` 119 | - **Back end**: `https://api.starter-next.ddev.com` 120 | 121 | If you would like to use different URLs, you must make a few changes in `.ddev/config.yaml`: 122 | 123 | - Update the `name` key (this influences the `starter-project` segment of the base URL); 124 | - Change the back-end hostname under the `additional_hostnames` key; 125 | - Change the `VIRTUAL_HOST` domains under `web_environment`; 126 | 127 | Then, a change is required for each of the nginx configuration files: 128 | 129 | - Change the `server_name` directive in `.ddev/nginx_full/api-site.conf` to match the back-end url; 130 | - Change the `server_name` directive in `.ddev/nginx_full/next-site.conf` to match the front-end url; 131 | 132 | Next.js also needs to be told what front-end URLs should look like: 133 | 134 | - Update `BASE_URL` in `frontend/.env`; 135 | 136 | Finally, Craft may need to generate absolute URLs to the control panel in some scenarios: 137 | 138 | - Update `CRAFT_BASE_CP_URL` in `backend/.env`; 139 | 140 | Your production configuration will probably look different—as long as Next.js knows where the GraphQL endpoint lives (`CRAFT_URL` in `frontend/.env`) and both Craft and Next.js know how to generate public URLs (`PRIMARY_SITE_URL` in `backend/.env` and `BASE_URL` in `frontend/.env`, respectively) these URLs don’t need to be related in any specific way! 141 | 142 | > [!TIP] 143 | > Always validate your CORS policy when deploying projects that make cross-domain requests! 144 | 145 | The _path_ Next.js uses to fetch data via GraphQL must be kept in sync between `backend/config/routes.php` and the `apiBasePath` variable in `frontend/src/lib/graphql.js`. 146 | 147 | ## Contributing 148 | 149 | Pull requests are welcome. Significant structural or aesthetic changes should be submitted as an [issue](https://github.com/craftcms/starter-next/issues). 150 | 151 | ## License 152 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | # These variables are only available to the Craft back-end! 2 | # See frontend/.env to configure the Nuxt development server. 3 | 4 | # Read about configuration, here: 5 | # https://craftcms.com/docs/5.x/configure.html 6 | 7 | # The application ID used to to uniquely store session and cache data, mutex locks, and more 8 | CRAFT_APP_ID= 9 | 10 | # The environment Craft is currently running in (dev, staging, production, etc.) 11 | CRAFT_ENVIRONMENT=dev 12 | 13 | # Database connection settings 14 | CRAFT_DB_DRIVER=mysql 15 | CRAFT_DB_SERVER=127.0.0.1 16 | CRAFT_DB_PORT=3306 17 | CRAFT_DB_DATABASE= 18 | CRAFT_DB_USER=root 19 | CRAFT_DB_PASSWORD= 20 | CRAFT_DB_SCHEMA=public 21 | CRAFT_DB_TABLE_PREFIX= 22 | 23 | # General settings 24 | # https://craftcms.com/docs/5.x/reference/config/general.html 25 | CRAFT_SECURITY_KEY= 26 | CRAFT_DEV_MODE=true 27 | CRAFT_ALLOW_ADMIN_CHANGES=true 28 | CRAFT_DISALLOW_ROBOTS=true 29 | CRAFT_BASE_CP_URL="https://api.starter-next.ddev.site" 30 | 31 | # Domain Configuration 32 | PRIMARY_SITE_URL="https://starter-next.ddev.site" 33 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | /.env 2 | /.idea 3 | /vendor 4 | .DS_Store 5 | /web/uploads 6 | -------------------------------------------------------------------------------- /backend/LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /backend/bootstrap.php: -------------------------------------------------------------------------------- 1 | safeLoad(); 18 | } 19 | -------------------------------------------------------------------------------- /backend/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "craftcms/craft", 3 | "description": "Craft CMS", 4 | "keywords": [ 5 | "craft", 6 | "cms", 7 | "craftcms", 8 | "project" 9 | ], 10 | "license": "0BSD", 11 | "homepage": "https://craftcms.com/", 12 | "type": "project", 13 | "support": { 14 | "email": "support@craftcms.com", 15 | "issues": "https://github.com/craftcms/cms/issues", 16 | "forum": "https://craftcms.stackexchange.com/", 17 | "source": "https://github.com/craftcms/cms", 18 | "docs": "https://craftcms.com/docs", 19 | "rss": "https://craftcms.com/changelog.rss" 20 | }, 21 | "minimum-stability": "dev", 22 | "prefer-stable": true, 23 | "require": { 24 | "craftcms/ckeditor": "^4.7.0", 25 | "craftcms/cloud": "*", 26 | "craftcms/cms": "^5.7.3", 27 | "vlucas/phpdotenv": "^5.4.0" 28 | }, 29 | "require-dev": { 30 | "craftcms/generator": "^2.0.0", 31 | "yiisoft/yii2-shell": "^2.0.3" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "modules\\": "modules/" 36 | } 37 | }, 38 | "config": { 39 | "allow-plugins": { 40 | "craftcms/plugin-installer": true, 41 | "yiisoft/yii2-composer": true 42 | }, 43 | "sort-packages": true, 44 | "optimize-autoloader": true, 45 | "platform": { 46 | "php": "8.2" 47 | } 48 | }, 49 | "scripts": { 50 | "post-update-cmd": [ 51 | "@php craft migrate/all --interactive=0", 52 | "@php craft project-config/apply", 53 | "@php craft clear-caches/all" 54 | ] 55 | }, 56 | "repositories": [ 57 | { 58 | "type": "composer", 59 | "url": "https://composer.craftcms.com", 60 | "canonical": false 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /backend/config/app.php: -------------------------------------------------------------------------------- 1 | App::env('CRAFT_APP_ID') ?: 'CraftCMS', 27 | 'modules' => [ 28 | 'authorinfo' => modules\authorinfo\Module::class, 29 | ], 30 | 'bootstrap' => ['authorinfo'], 31 | ]; 32 | -------------------------------------------------------------------------------- /backend/config/app.web.php: -------------------------------------------------------------------------------- 1 | [ 8 | 'class' => Cors::class, 9 | 'cors' => [ 10 | 'Origin' => [ 11 | App::env('PRIMARY_SITE_URL') 12 | ], 13 | 'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], 14 | 'Access-Control-Request-Headers' => ['*'], 15 | 'Access-Control-Allow-Credentials' => true, 16 | 'Access-Control-Max-Age' => 86400, 17 | 'Access-Control-Expose-Headers' => [], 18 | ] 19 | ] 20 | ]; 21 | -------------------------------------------------------------------------------- /backend/config/general.php: -------------------------------------------------------------------------------- 1 | defaultWeekStartDay(1) 17 | // Prevent generated URLs from including "index.php" 18 | ->omitScriptNameInUrls() 19 | // Preload Single entries as Twig variables 20 | ->preloadSingles() 21 | // Prevent user enumeration attacks 22 | ->preventUserEnumeration() 23 | // Set the @webroot alias so the clear-caches command knows where to find CP resources 24 | ->aliases([ 25 | '@webroot' => dirname(__DIR__) . '/web', 26 | '@preview' => App::env('PRIMARY_SITE_URL'), 27 | ]) 28 | ->headlessMode(); 29 | -------------------------------------------------------------------------------- /backend/config/htmlpurifier/Default.json: -------------------------------------------------------------------------------- 1 | { 2 | "Attr.AllowedFrameTargets": [ 3 | "_blank" 4 | ], 5 | "Attr.EnableID": true, 6 | "HTML.AllowedComments": [ 7 | "pagebreak" 8 | ], 9 | "HTML.SafeIframe": true, 10 | "URI.SafeIframeRegexp": "%^(https?:)?//(www.youtube.com/|player.vimeo.com/)%" 11 | } 12 | -------------------------------------------------------------------------------- /backend/config/project/ckeditor/configs/7dac7ab3-08a0-4af8-ba7c-cbecd0726f08.yaml: -------------------------------------------------------------------------------- 1 | headingLevels: 2 | - 1 3 | - 2 4 | - 3 5 | - 4 6 | - 5 7 | - 6 8 | name: Simple 9 | toolbar: 10 | - heading 11 | - '|' 12 | - bold 13 | - italic 14 | - link 15 | -------------------------------------------------------------------------------- /backend/config/project/ckeditor/configs/b579b454-12bd-4710-a436-c26907141957.yaml: -------------------------------------------------------------------------------- 1 | headingLevels: 2 | - 1 3 | - 2 4 | - 3 5 | - 4 6 | - 5 7 | - 6 8 | name: Full 9 | options: 10 | alignment: 11 | options: 12 | - left 13 | - right 14 | fontSize: 15 | options: 16 | - tiny 17 | - small 18 | - big 19 | - huge 20 | style: 21 | definitions: 22 | - 23 | classes: 24 | - red-heading 25 | element: h2 26 | name: 'Red heading' 27 | - 28 | classes: 29 | - vibrant-code 30 | element: pre 31 | name: 'Vibrant code' 32 | - 33 | classes: 34 | - marker 35 | element: span 36 | name: Marker 37 | toolbar: 38 | - undo 39 | - redo 40 | - selectAll 41 | - findAndReplace 42 | - '|' 43 | - createEntry 44 | - heading 45 | - '|' 46 | - fontSize 47 | - fontFamily 48 | - fontColor 49 | - fontBackgroundColor 50 | - '|' 51 | - bold 52 | - italic 53 | - underline 54 | - strikethrough 55 | - subscript 56 | - superscript 57 | - code 58 | - codeBlock 59 | - link 60 | - bulletedList 61 | - numberedList 62 | - todoList 63 | - '|' 64 | - alignment 65 | - outdent 66 | - indent 67 | - '|' 68 | - anchor 69 | - insertTable 70 | - insertImage 71 | - blockQuote 72 | - mediaEmbed 73 | - htmlEmbed 74 | - pageBreak 75 | - horizontalLine 76 | - '|' 77 | - style 78 | - textPartLanguage 79 | - '|' 80 | - removeFormat 81 | - '|' 82 | - sourceEditing 83 | -------------------------------------------------------------------------------- /backend/config/project/entryTypes/callout--f7576aa3-c155-4b54-82c9-8cb8c93495c9.yaml: -------------------------------------------------------------------------------- 1 | color: blue 2 | fieldLayouts: 3 | 7c8f65e9-675e-4535-bab7-77e644a81e96: 4 | tabs: 5 | - 6 | elementCondition: null 7 | elements: 8 | - 9 | autocapitalize: true 10 | autocomplete: false 11 | autocorrect: true 12 | class: null 13 | dateAdded: '2024-08-28T04:41:30+00:00' 14 | disabled: false 15 | elementCondition: null 16 | id: null 17 | includeInCards: false 18 | inputType: null 19 | instructions: null 20 | label: null 21 | max: null 22 | min: null 23 | name: null 24 | orientation: null 25 | placeholder: null 26 | providesThumbs: false 27 | readonly: false 28 | requirable: false 29 | size: null 30 | step: null 31 | tip: null 32 | title: null 33 | type: craft\fieldlayoutelements\entries\EntryTitleField 34 | uid: 1f0911fa-41a3-4430-9c79-6326df8f714f 35 | userCondition: null 36 | warning: null 37 | width: 100 38 | - 39 | dateAdded: '2024-08-28T04:43:19+00:00' 40 | elementCondition: null 41 | fieldUid: 0a58e287-e038-4eb7-ab2a-cf4cbdbe013d # Page content 42 | handle: null 43 | includeInCards: false 44 | instructions: null 45 | label: null 46 | providesThumbs: false 47 | required: false 48 | tip: null 49 | type: craft\fieldlayoutelements\CustomField 50 | uid: eb15c0fc-cdad-4ab0-91a1-c92354ff52f0 51 | userCondition: null 52 | warning: null 53 | width: 100 54 | name: Content 55 | uid: 57ee79bd-1509-4d6a-9f60-400eb6ee7280 56 | userCondition: null 57 | handle: callout 58 | hasTitleField: false 59 | icon: square 60 | name: Callout 61 | showSlugField: false 62 | showStatusField: false 63 | slugTranslationKeyFormat: null 64 | slugTranslationMethod: site 65 | titleFormat: null 66 | titleTranslationKeyFormat: null 67 | titleTranslationMethod: site 68 | -------------------------------------------------------------------------------- /backend/config/project/entryTypes/category--5dd08fc8-3ea4-4582-952f-4c0f7ebf409e.yaml: -------------------------------------------------------------------------------- 1 | color: amber 2 | fieldLayouts: 3 | 97b6299e-a61f-4649-95ea-b5fec0fa4345: 4 | tabs: 5 | - 6 | elementCondition: null 7 | elements: 8 | - 9 | autocapitalize: true 10 | autocomplete: false 11 | autocorrect: true 12 | class: null 13 | dateAdded: '2024-08-27T07:14:50+00:00' 14 | disabled: false 15 | elementCondition: null 16 | id: null 17 | includeInCards: false 18 | inputType: null 19 | instructions: null 20 | label: null 21 | max: null 22 | min: null 23 | name: null 24 | orientation: null 25 | placeholder: null 26 | providesThumbs: false 27 | readonly: false 28 | requirable: false 29 | size: null 30 | step: null 31 | tip: null 32 | title: null 33 | type: craft\fieldlayoutelements\entries\EntryTitleField 34 | uid: fd6c33c0-568e-45cb-b43f-6e7b1fce0f56 35 | userCondition: null 36 | warning: null 37 | width: 100 38 | - 39 | dateAdded: '2024-08-27T07:16:53+00:00' 40 | elementCondition: null 41 | fieldUid: 9d42aa56-4a66-4d0b-b8a9-84a4cb387788 # Text block 42 | handle: description 43 | includeInCards: false 44 | instructions: null 45 | label: Description 46 | providesThumbs: false 47 | required: false 48 | tip: null 49 | type: craft\fieldlayoutelements\CustomField 50 | uid: f0cedd90-29b8-4cad-9a11-f490c0868298 51 | userCondition: null 52 | warning: null 53 | width: 100 54 | name: Content 55 | uid: 6803fe6d-73a8-4ab5-a27a-b44b20f843a3 56 | userCondition: null 57 | handle: category 58 | hasTitleField: true 59 | icon: icons 60 | name: Category 61 | showSlugField: true 62 | showStatusField: true 63 | slugTranslationKeyFormat: null 64 | slugTranslationMethod: site 65 | titleFormat: null 66 | titleTranslationKeyFormat: null 67 | titleTranslationMethod: site 68 | -------------------------------------------------------------------------------- /backend/config/project/entryTypes/global--d55d172f-8b8f-4bb7-b680-31249981c350.yaml: -------------------------------------------------------------------------------- 1 | color: null 2 | fieldLayouts: 3 | c250a111-95a7-4027-9739-9ff603802589: 4 | tabs: 5 | - 6 | elementCondition: null 7 | elements: 8 | - 9 | dateAdded: '2024-08-21T04:09:37+00:00' 10 | elementCondition: null 11 | fieldUid: 6c9778b8-507c-4108-937d-6ceeabf35047 # Image 12 | handle: logo 13 | includeInCards: false 14 | instructions: null 15 | label: Logo 16 | providesThumbs: false 17 | required: false 18 | tip: null 19 | type: craft\fieldlayoutelements\CustomField 20 | uid: 3ca47574-03ee-4e53-b004-c2184280db36 21 | userCondition: null 22 | warning: null 23 | width: 100 24 | - 25 | dateAdded: '2024-08-21T03:11:22+00:00' 26 | elementCondition: null 27 | fieldUid: 163c895f-0c05-4845-80e2-043b2346d830 # Address 28 | handle: null 29 | includeInCards: false 30 | instructions: null 31 | label: null 32 | providesThumbs: false 33 | required: false 34 | tip: null 35 | type: craft\fieldlayoutelements\CustomField 36 | uid: 6b8e65a2-d3b2-4bc3-a341-bcecd50aaf4b 37 | userCondition: null 38 | warning: null 39 | width: 100 40 | name: Header 41 | uid: e59c6a9e-e2c2-4738-88f5-61ee419cf02e 42 | userCondition: null 43 | handle: global 44 | hasTitleField: false 45 | icon: globe 46 | name: Global 47 | showSlugField: false 48 | showStatusField: false 49 | slugTranslationKeyFormat: null 50 | slugTranslationMethod: site 51 | titleFormat: Global 52 | titleTranslationKeyFormat: null 53 | titleTranslationMethod: site 54 | -------------------------------------------------------------------------------- /backend/config/project/entryTypes/image--5358db38-27c0-49b4-b9d3-e68326e79da2.yaml: -------------------------------------------------------------------------------- 1 | color: rose 2 | fieldLayouts: 3 | 7339b416-d8d5-4e30-8a83-38965d1fbab7: 4 | tabs: 5 | - 6 | elementCondition: null 7 | elements: 8 | - 9 | autocapitalize: true 10 | autocomplete: false 11 | autocorrect: true 12 | class: null 13 | dateAdded: '2024-08-20T09:03:27+00:00' 14 | disabled: false 15 | elementCondition: null 16 | id: null 17 | includeInCards: false 18 | inputType: null 19 | instructions: null 20 | label: null 21 | max: null 22 | min: null 23 | name: null 24 | orientation: null 25 | placeholder: null 26 | providesThumbs: false 27 | readonly: false 28 | requirable: false 29 | size: null 30 | step: null 31 | tip: null 32 | title: null 33 | type: craft\fieldlayoutelements\entries\EntryTitleField 34 | uid: b86ad9e7-21b8-4f74-8016-5470aec58033 35 | userCondition: null 36 | warning: null 37 | width: 100 38 | - 39 | dateAdded: '2024-08-20T09:04:57+00:00' 40 | elementCondition: null 41 | fieldUid: 6c9778b8-507c-4108-937d-6ceeabf35047 # Image 42 | handle: null 43 | includeInCards: false 44 | instructions: null 45 | label: null 46 | providesThumbs: false 47 | required: false 48 | tip: null 49 | type: craft\fieldlayoutelements\CustomField 50 | uid: c5ab7afb-b942-4ca5-bda8-585e8d0aff03 51 | userCondition: null 52 | warning: null 53 | width: 100 54 | name: Content 55 | uid: 2491a701-f307-4b27-93a1-b24fcca58841 56 | userCondition: null 57 | handle: image 58 | hasTitleField: false 59 | icon: image 60 | name: Image 61 | showSlugField: false 62 | showStatusField: false 63 | slugTranslationKeyFormat: null 64 | slugTranslationMethod: site 65 | titleFormat: null 66 | titleTranslationKeyFormat: null 67 | titleTranslationMethod: site 68 | -------------------------------------------------------------------------------- /backend/config/project/entryTypes/page--697da4c2-e02f-4f49-9d76-e22609ceda25.yaml: -------------------------------------------------------------------------------- 1 | color: null 2 | fieldLayouts: 3 | c89336c4-080e-4733-8042-a2fe82bb0884: 4 | tabs: 5 | - 6 | elementCondition: null 7 | elements: 8 | - 9 | autocapitalize: true 10 | autocomplete: false 11 | autocorrect: true 12 | class: null 13 | dateAdded: '2024-08-20T08:30:03+00:00' 14 | disabled: false 15 | elementCondition: null 16 | id: null 17 | includeInCards: false 18 | inputType: null 19 | instructions: null 20 | label: null 21 | max: null 22 | min: null 23 | name: null 24 | orientation: null 25 | placeholder: null 26 | providesThumbs: false 27 | readonly: false 28 | requirable: false 29 | size: null 30 | step: null 31 | tip: null 32 | title: null 33 | type: craft\fieldlayoutelements\entries\EntryTitleField 34 | uid: 7c51ef0b-1873-4da0-94e8-a59e7dbd0343 35 | userCondition: null 36 | warning: null 37 | width: 100 38 | - 39 | dateAdded: '2024-08-21T04:29:15+00:00' 40 | elementCondition: null 41 | fieldUid: 6d2e5577-5ded-43c9-96e2-d4fc4c770fe9 # Text 42 | handle: pageSubheading 43 | includeInCards: false 44 | instructions: null 45 | label: 'Page subheading' 46 | providesThumbs: false 47 | required: false 48 | tip: null 49 | type: craft\fieldlayoutelements\CustomField 50 | uid: 225541d2-66a5-4775-958a-af5304fa9875 51 | userCondition: null 52 | warning: null 53 | width: 100 54 | - 55 | dateAdded: '2024-08-20T09:36:26+00:00' 56 | elementCondition: null 57 | fieldUid: 6c9778b8-507c-4108-937d-6ceeabf35047 # Image 58 | handle: null 59 | includeInCards: false 60 | instructions: null 61 | label: 'Hero image' 62 | providesThumbs: false 63 | required: false 64 | tip: null 65 | type: craft\fieldlayoutelements\CustomField 66 | uid: afc63364-6f55-4d6f-99e4-b6bae3ba3f57 67 | userCondition: null 68 | warning: null 69 | width: 100 70 | - 71 | dateAdded: '2024-08-27T07:20:15+00:00' 72 | elementCondition: 73 | class: craft\elements\conditions\entries\EntryCondition 74 | conditionRules: 75 | - 76 | class: craft\elements\conditions\entries\SectionConditionRule 77 | operator: in 78 | uid: cce39d0f-1040-4e19-8a10-16b502f3149d 79 | values: 80 | - 6ba54824-1c3f-454f-8830-f6cea379bee4 # Blog Posts 81 | elementType: craft\elements\Entry 82 | fieldContext: global 83 | fieldUid: d390347a-09cf-4b70-a3b4-bc3cca520623 # Category 84 | handle: null 85 | includeInCards: false 86 | instructions: null 87 | label: null 88 | providesThumbs: false 89 | required: false 90 | tip: null 91 | type: craft\fieldlayoutelements\CustomField 92 | uid: 1b82e916-cf89-48af-8ad0-e5613d8bf46d 93 | userCondition: null 94 | warning: null 95 | width: 100 96 | - 97 | dateAdded: '2024-08-20T09:36:26+00:00' 98 | elementCondition: null 99 | fieldUid: 0a58e287-e038-4eb7-ab2a-cf4cbdbe013d # Page content 100 | handle: null 101 | includeInCards: false 102 | instructions: null 103 | label: null 104 | providesThumbs: false 105 | required: false 106 | tip: null 107 | type: craft\fieldlayoutelements\CustomField 108 | uid: e8c92b0c-8394-4f6e-b2ab-19c12217ced3 109 | userCondition: null 110 | warning: null 111 | width: 100 112 | name: Content 113 | uid: c2ab8c02-117e-4db0-924c-3e3ba5dae762 114 | userCondition: null 115 | handle: page 116 | hasTitleField: true 117 | icon: file 118 | name: Page 119 | showSlugField: true 120 | showStatusField: true 121 | slugTranslationKeyFormat: null 122 | slugTranslationMethod: site 123 | titleFormat: null 124 | titleTranslationKeyFormat: null 125 | titleTranslationMethod: site 126 | -------------------------------------------------------------------------------- /backend/config/project/entryTypes/text--25b2446d-c1d2-4aa1-8184-3147e617f069.yaml: -------------------------------------------------------------------------------- 1 | color: violet 2 | fieldLayouts: 3 | f00ef8d9-b10c-4a28-a60e-5277f76636f3: 4 | tabs: 5 | - 6 | elementCondition: null 7 | elements: 8 | - 9 | autocapitalize: true 10 | autocomplete: false 11 | autocorrect: true 12 | class: null 13 | dateAdded: '2024-08-20T09:01:40+00:00' 14 | disabled: false 15 | elementCondition: null 16 | id: null 17 | includeInCards: false 18 | inputType: null 19 | instructions: null 20 | label: null 21 | max: null 22 | min: null 23 | name: null 24 | orientation: null 25 | placeholder: null 26 | providesThumbs: false 27 | readonly: false 28 | requirable: false 29 | size: null 30 | step: null 31 | tip: null 32 | title: null 33 | type: craft\fieldlayoutelements\entries\EntryTitleField 34 | uid: 434f527a-61b8-4386-a9d5-351aa02db45b 35 | userCondition: null 36 | warning: null 37 | width: 100 38 | - 39 | dateAdded: '2024-08-20T09:04:25+00:00' 40 | elementCondition: null 41 | fieldUid: 9d42aa56-4a66-4d0b-b8a9-84a4cb387788 # Text block 42 | handle: null 43 | includeInCards: false 44 | instructions: null 45 | label: null 46 | providesThumbs: false 47 | required: false 48 | tip: null 49 | type: craft\fieldlayoutelements\CustomField 50 | uid: ff45a2cd-9b8f-492e-b70f-49d7fdbcdce8 51 | userCondition: null 52 | warning: null 53 | width: 100 54 | name: Content 55 | uid: ea21cde6-ed6d-4134-b16c-97380c9cfb97 56 | userCondition: null 57 | handle: text 58 | hasTitleField: true 59 | icon: receipt 60 | name: Text 61 | showSlugField: false 62 | showStatusField: false 63 | slugTranslationKeyFormat: null 64 | slugTranslationMethod: site 65 | titleFormat: 'Post {now}' 66 | titleTranslationKeyFormat: null 67 | titleTranslationMethod: site 68 | -------------------------------------------------------------------------------- /backend/config/project/entryTypes/video--a06e695e-af7f-4a1b-84a7-575ec935e293.yaml: -------------------------------------------------------------------------------- 1 | color: orange 2 | fieldLayouts: 3 | a1ab1ec3-213d-4bba-8215-4b7b456111a6: 4 | tabs: 5 | - 6 | elementCondition: null 7 | elements: 8 | - 9 | autocapitalize: true 10 | autocomplete: false 11 | autocorrect: true 12 | class: null 13 | dateAdded: '2024-08-20T09:10:56+00:00' 14 | disabled: false 15 | elementCondition: null 16 | id: null 17 | includeInCards: false 18 | inputType: null 19 | instructions: null 20 | label: null 21 | max: null 22 | min: null 23 | name: null 24 | orientation: null 25 | placeholder: null 26 | providesThumbs: false 27 | readonly: false 28 | requirable: false 29 | size: null 30 | step: null 31 | tip: null 32 | title: null 33 | type: craft\fieldlayoutelements\entries\EntryTitleField 34 | uid: 3bdf0c7d-0d25-4f7c-8287-59415b4b5c0c 35 | userCondition: null 36 | warning: null 37 | width: 100 38 | - 39 | dateAdded: '2024-08-20T09:12:38+00:00' 40 | elementCondition: null 41 | fieldUid: 9d082133-4710-4468-ab39-80db8c9fcf59 # Code 42 | handle: embedCode 43 | includeInCards: false 44 | instructions: null 45 | label: 'Embed code' 46 | providesThumbs: false 47 | required: false 48 | tip: null 49 | type: craft\fieldlayoutelements\CustomField 50 | uid: e34073d1-9f80-4e54-a691-6f32a694a432 51 | userCondition: null 52 | warning: null 53 | width: 100 54 | name: Content 55 | uid: 39d66e39-f720-4104-a64a-1482282eb0e7 56 | userCondition: null 57 | handle: video 58 | hasTitleField: false 59 | icon: film 60 | name: Video 61 | showSlugField: false 62 | showStatusField: false 63 | slugTranslationKeyFormat: null 64 | slugTranslationMethod: site 65 | titleFormat: null 66 | titleTranslationKeyFormat: null 67 | titleTranslationMethod: site 68 | -------------------------------------------------------------------------------- /backend/config/project/fields/address--163c895f-0c05-4845-80e2-043b2346d830.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: address 3 | instructions: null 4 | name: Address 5 | searchable: false 6 | settings: 7 | maxAddresses: null 8 | minAddresses: null 9 | viewMode: cards 10 | translationKeyFormat: null 11 | translationMethod: site 12 | type: craft\fields\Addresses 13 | -------------------------------------------------------------------------------- /backend/config/project/fields/category--d390347a-09cf-4b70-a3b4-bc3cca520623.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: category 3 | instructions: null 4 | name: Category 5 | searchable: false 6 | settings: 7 | allowSelfRelations: false 8 | branchLimit: 1 9 | maintainHierarchy: true 10 | maxRelations: null 11 | minRelations: null 12 | selectionLabel: 'Add category' 13 | showCardsInGrid: false 14 | showSiteMenu: false 15 | sources: 16 | - 'section:70b2614b-639a-4de2-8c11-3851d36d6f8c' # Blog Categories 17 | targetSiteId: null 18 | validateRelatedElements: false 19 | viewMode: list 20 | translationKeyFormat: null 21 | translationMethod: none 22 | type: craft\fields\Entries 23 | -------------------------------------------------------------------------------- /backend/config/project/fields/code--9d082133-4710-4468-ab39-80db8c9fcf59.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: code 3 | instructions: null 4 | name: Code 5 | searchable: false 6 | settings: 7 | byteLimit: null 8 | charLimit: null 9 | code: true 10 | initialRows: 4 11 | multiline: true 12 | placeholder: null 13 | uiMode: normal 14 | translationKeyFormat: null 15 | translationMethod: none 16 | type: craft\fields\PlainText 17 | -------------------------------------------------------------------------------- /backend/config/project/fields/dividerType--6c819fc7-3f73-4873-8f0c-5ea322fef1f9.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: dividerType 3 | instructions: null 4 | name: 'Divider type' 5 | searchable: false 6 | settings: 7 | options: 8 | - 9 | __assoc__: 10 | - 11 | - label 12 | - Rule 13 | - 14 | - value 15 | - rule 16 | - 17 | - default 18 | - '' 19 | - 20 | __assoc__: 21 | - 22 | - label 23 | - Space 24 | - 25 | - value 26 | - space 27 | - 28 | - default 29 | - '' 30 | translationKeyFormat: null 31 | translationMethod: none 32 | type: craft\fields\Dropdown 33 | -------------------------------------------------------------------------------- /backend/config/project/fields/image--6c9778b8-507c-4108-937d-6ceeabf35047.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: image 3 | instructions: null 4 | name: Image 5 | searchable: false 6 | settings: 7 | allowSelfRelations: false 8 | allowSubfolders: true 9 | allowUploads: true 10 | allowedKinds: 11 | - image 12 | branchLimit: null 13 | defaultUploadLocationSource: 'volume:7b9364bd-2215-495a-8b0b-2280df887a8b' # Media 14 | defaultUploadLocationSubpath: null 15 | maintainHierarchy: false 16 | maxRelations: null 17 | minRelations: null 18 | previewMode: full 19 | restrictFiles: true 20 | restrictLocation: true 21 | restrictedDefaultUploadSubpath: null 22 | restrictedLocationSource: 'volume:7b9364bd-2215-495a-8b0b-2280df887a8b' # Media 23 | restrictedLocationSubpath: null 24 | selectionLabel: null 25 | showCardsInGrid: false 26 | showSiteMenu: true 27 | showUnpermittedFiles: false 28 | showUnpermittedVolumes: false 29 | sources: '*' 30 | targetSiteId: null 31 | validateRelatedElements: true 32 | viewMode: list 33 | translationKeyFormat: null 34 | translationMethod: none 35 | type: craft\fields\Assets 36 | -------------------------------------------------------------------------------- /backend/config/project/fields/navigation--2bae9725-9546-4f55-a889-56102191aa89.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: navigation 3 | instructions: null 4 | name: Navigation 5 | searchable: false 6 | settings: 7 | createButtonLabel: 'New page' 8 | includeTableView: false 9 | maxEntries: null 10 | minEntries: null 11 | pageSize: 50 12 | propagationKeyFormat: null 13 | propagationMethod: all 14 | showCardsInGrid: false 15 | viewMode: cards 16 | translationKeyFormat: null 17 | translationMethod: site 18 | type: craft\fields\Matrix 19 | -------------------------------------------------------------------------------- /backend/config/project/fields/page--d4f0917b-edcf-4ef0-a98b-661ff27384ad.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: page 3 | instructions: null 4 | name: Page 5 | searchable: false 6 | settings: 7 | allowSelfRelations: false 8 | branchLimit: null 9 | maintainHierarchy: false 10 | maxRelations: 1 11 | minRelations: 1 12 | selectionLabel: 'Add page' 13 | showCardsInGrid: false 14 | showSiteMenu: true 15 | sources: 16 | - singles 17 | targetSiteId: null 18 | validateRelatedElements: false 19 | viewMode: list 20 | translationKeyFormat: null 21 | translationMethod: none 22 | type: craft\fields\Entries 23 | -------------------------------------------------------------------------------- /backend/config/project/fields/pageContent--0a58e287-e038-4eb7-ab2a-cf4cbdbe013d.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: pageContent 3 | instructions: null 4 | name: 'Page content' 5 | searchable: true 6 | settings: 7 | availableTransforms: 8 | - 0922209d-2911-419e-82cf-b05113a35f6e # Wide 9 | availableVolumes: 10 | - 7b9364bd-2215-495a-8b0b-2280df887a8b # Media 11 | ckeConfig: b579b454-12bd-4710-a436-c26907141957 # Full 12 | createButtonLabel: 'New block' 13 | defaultTransform: null 14 | enableSourceEditingForNonAdmins: false 15 | entryTypes: 16 | - a06e695e-af7f-4a1b-84a7-575ec935e293 # Video 17 | - 5358db38-27c0-49b4-b9d3-e68326e79da2 # Image 18 | - f7576aa3-c155-4b54-82c9-8cb8c93495c9 # Callout 19 | purifierConfig: null 20 | purifyHtml: true 21 | showUnpermittedFiles: false 22 | showUnpermittedVolumes: false 23 | showWordCount: true 24 | wordLimit: null 25 | translationKeyFormat: null 26 | translationMethod: none 27 | type: craft\ckeditor\Field 28 | -------------------------------------------------------------------------------- /backend/config/project/fields/text--6d2e5577-5ded-43c9-96e2-d4fc4c770fe9.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: text 3 | instructions: null 4 | name: Text 5 | searchable: true 6 | settings: 7 | byteLimit: null 8 | charLimit: null 9 | code: false 10 | initialRows: 4 11 | multiline: false 12 | placeholder: null 13 | uiMode: normal 14 | translationKeyFormat: null 15 | translationMethod: none 16 | type: craft\fields\PlainText 17 | -------------------------------------------------------------------------------- /backend/config/project/fields/textBlock--9d42aa56-4a66-4d0b-b8a9-84a4cb387788.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: textBlock 3 | instructions: null 4 | name: 'Text block' 5 | searchable: true 6 | settings: 7 | byteLimit: null 8 | charLimit: null 9 | code: false 10 | initialRows: 20 11 | multiline: true 12 | placeholder: null 13 | uiMode: normal 14 | translationKeyFormat: null 15 | translationMethod: none 16 | type: craft\fields\PlainText 17 | -------------------------------------------------------------------------------- /backend/config/project/graphql/graphql.yaml: -------------------------------------------------------------------------------- 1 | publicToken: 2 | enabled: true 3 | expiryDate: null 4 | -------------------------------------------------------------------------------- /backend/config/project/graphql/schemas/2b829e15-a7bd-4ab0-9050-dcdfe44499cf.yaml: -------------------------------------------------------------------------------- 1 | isPublic: true 2 | name: 'Public Schema' 3 | scope: 4 | - 'sites.58ccb571-6c72-449f-8e7b-6f43c5d416b3:read' # Next Starter 5 | - 'elements.drafts:read' 6 | - 'elements.revisions:read' 7 | - 'elements.inactive:read' 8 | - 'sections.998bf681-e007-4f7e-a525-863e01a38560:read' # Blog 9 | - 'sections.bd99645a-4524-40e4-83dc-c6b12e023bab:read' # Global 10 | - 'sections.aef20f9c-5c84-4c50-8a18-acf0452db62f:read' # Guestbook 11 | - 'sections.cdad2202-5d55-49ba-995e-962503f5887f:read' # Home 12 | - 'sections.70b2614b-639a-4de2-8c11-3851d36d6f8c:read' # Blog Categories 13 | - 'sections.6ba54824-1c3f-454f-8830-f6cea379bee4:read' # Blog Posts 14 | - 'sections.3e2a1a6f-88ea-477b-936d-39393f2d594f:read' # Guestbook Posts 15 | - 'sections.c4b4f50d-7d62-4a4c-bbf8-7aae00fd82c7:read' # Pages 16 | - 'nestedentryfields.0a58e287-e038-4eb7-ab2a-cf4cbdbe013d:read' # Page content 17 | - 'nestedentryfields.2bae9725-9546-4f55-a889-56102191aa89:read' # Navigation 18 | - 'volumes.7b9364bd-2215-495a-8b0b-2280df887a8b:read' # Media 19 | -------------------------------------------------------------------------------- /backend/config/project/graphql/schemas/c7d2eb61-cdde-4a76-88a9-eb30ddcf155b.yaml: -------------------------------------------------------------------------------- 1 | isPublic: false 2 | name: Guestbook 3 | scope: 4 | - 'sites.58ccb571-6c72-449f-8e7b-6f43c5d416b3:read' # Next Starter 5 | - 'sections.3e2a1a6f-88ea-477b-936d-39393f2d594f:read' # Guestbook Posts 6 | - 'sections.3e2a1a6f-88ea-477b-936d-39393f2d594f:edit' # Guestbook Posts 7 | - 'sections.3e2a1a6f-88ea-477b-936d-39393f2d594f:create' # Guestbook Posts 8 | -------------------------------------------------------------------------------- /backend/config/project/imageTransforms/avatar--b5b7bfc5-a39e-4d1a-89be-51f0afeb5ce0.yaml: -------------------------------------------------------------------------------- 1 | fill: null 2 | format: null 3 | handle: avatar 4 | height: 1024 5 | interlace: plane 6 | mode: crop 7 | name: Avatar 8 | position: center-center 9 | quality: null 10 | upscale: true 11 | width: 1024 12 | -------------------------------------------------------------------------------- /backend/config/project/imageTransforms/hero--0413e9dc-1c8a-4221-b81d-c9f6c0594cbd.yaml: -------------------------------------------------------------------------------- 1 | fill: null 2 | format: webp 3 | handle: hero 4 | height: 1080 5 | interlace: none 6 | mode: crop 7 | name: Hero 8 | position: center-center 9 | quality: 60 10 | upscale: true 11 | width: 2048 12 | -------------------------------------------------------------------------------- /backend/config/project/imageTransforms/wide--0922209d-2911-419e-82cf-b05113a35f6e.yaml: -------------------------------------------------------------------------------- 1 | fill: null 2 | format: null 3 | handle: wide 4 | height: 1080 5 | interlace: plane 6 | mode: crop 7 | name: Wide 8 | position: center-center 9 | quality: 30 10 | upscale: true 11 | width: 1920 12 | -------------------------------------------------------------------------------- /backend/config/project/project.yaml: -------------------------------------------------------------------------------- 1 | dateModified: 1744760819 2 | elementSources: 3 | craft\elements\Entry: 4 | - 5 | condition: 6 | class: craft\elements\conditions\entries\EntryCondition 7 | conditionRules: 8 | - 9 | class: craft\elements\conditions\SlugConditionRule 10 | operator: '=' 11 | uid: d81e0ca8-0443-4c79-a338-14e2c1fc8687 12 | value: global 13 | elementType: craft\elements\Entry 14 | fieldContext: global 15 | defaultSort: 16 | - title 17 | - asc 18 | key: 'custom:6d0e2f9b-417a-4d0c-b23f-affe43c388c0' 19 | label: Global 20 | tableAttributes: '-' 21 | type: custom 22 | - 23 | heading: Pages 24 | type: heading 25 | - 26 | condition: 27 | class: craft\elements\conditions\entries\EntryCondition 28 | conditionRules: 29 | - 30 | class: craft\elements\conditions\entries\SectionConditionRule 31 | operator: in 32 | uid: 5690305a-9541-42b2-9e20-e0f68d6f7e30 33 | values: 34 | - 998bf681-e007-4f7e-a525-863e01a38560 # Blog 35 | - aef20f9c-5c84-4c50-8a18-acf0452db62f # Guestbook 36 | - cdad2202-5d55-49ba-995e-962503f5887f # Home 37 | elementType: craft\elements\Entry 38 | fieldContext: global 39 | defaultSort: 40 | - title 41 | - asc 42 | key: 'custom:bef75f27-face-4b86-af9e-61d96538bfed' 43 | label: 'Default pages' 44 | tableAttributes: '-' 45 | type: custom 46 | - 47 | defaultSort: 48 | - structure 49 | - asc 50 | disabled: false 51 | key: 'section:c4b4f50d-7d62-4a4c-bbf8-7aae00fd82c7' # Pages 52 | tableAttributes: 53 | - status 54 | - postDate 55 | - expiryDate 56 | - link 57 | type: native 58 | - 59 | heading: Channels 60 | type: heading 61 | - 62 | defaultSort: 63 | - postDate 64 | - desc 65 | disabled: false 66 | key: 'section:6ba54824-1c3f-454f-8830-f6cea379bee4' # Blog Posts 67 | tableAttributes: 68 | - status 69 | - postDate 70 | - expiryDate 71 | - link 72 | type: native 73 | - 74 | key: 'section:70b2614b-639a-4de2-8c11-3851d36d6f8c' # Blog Categories 75 | type: native 76 | - 77 | defaultSort: 78 | - postDate 79 | - desc 80 | disabled: false 81 | key: 'section:3e2a1a6f-88ea-477b-936d-39393f2d594f' # Guestbook Posts 82 | tableAttributes: 83 | - status 84 | - postDate 85 | - expiryDate 86 | - link 87 | type: native 88 | - 89 | heading: Everything 90 | type: heading 91 | - 92 | defaultSort: 93 | - title 94 | - asc 95 | disabled: true 96 | key: singles 97 | tableAttributes: 98 | - status 99 | - link 100 | type: native 101 | - 102 | defaultSort: 103 | - postDate 104 | - desc 105 | disabled: false 106 | key: '*' 107 | tableAttributes: 108 | - status 109 | - section 110 | - postDate 111 | - expiryDate 112 | - link 113 | type: native 114 | - 115 | heading: '' 116 | type: heading 117 | email: 118 | fromEmail: travis@pixelandtonic.com 119 | fromName: 'Starter starter' 120 | transportType: craft\mail\transportadapters\Sendmail 121 | fs: 122 | cloud: 123 | hasUrls: true 124 | name: Cloud 125 | settings: 126 | expires: '1 years' 127 | hasUrls: true 128 | localFsPath: '@webroot/uploads' 129 | localFsUrl: $CRAFT_BASE_CP_URL/uploads 130 | subpath: null 131 | type: craft\cloud\fs\AssetsFs 132 | meta: 133 | __names__: 134 | 0a58e287-e038-4eb7-ab2a-cf4cbdbe013d: 'Page content' # Page content 135 | 2b829e15-a7bd-4ab0-9050-dcdfe44499cf: 'Public Schema' # Public Schema 136 | 2bae9725-9546-4f55-a889-56102191aa89: Navigation # Navigation 137 | 3e2a1a6f-88ea-477b-936d-39393f2d594f: 'Guestbook Posts' # Guestbook Posts 138 | 5dd08fc8-3ea4-4582-952f-4c0f7ebf409e: Category # Category 139 | 6ba54824-1c3f-454f-8830-f6cea379bee4: 'Blog Posts' # Blog Posts 140 | 6c819fc7-3f73-4873-8f0c-5ea322fef1f9: 'Divider type' # Divider type 141 | 6c9778b8-507c-4108-937d-6ceeabf35047: Image # Image 142 | 6d0d3aa4-84be-441d-8fcc-2ab2d6db2503: Starters # Starters 143 | 6d2e5577-5ded-43c9-96e2-d4fc4c770fe9: Text # Text 144 | 7b9364bd-2215-495a-8b0b-2280df887a8b: Media # Media 145 | 7dac7ab3-08a0-4af8-ba7c-cbecd0726f08: Simple # Simple 146 | 9d082133-4710-4468-ab39-80db8c9fcf59: Code # Code 147 | 9d42aa56-4a66-4d0b-b8a9-84a4cb387788: 'Text block' # Text block 148 | 25b2446d-c1d2-4aa1-8184-3147e617f069: Text # Text 149 | 58ccb571-6c72-449f-8e7b-6f43c5d416b3: 'Next Starter' # Next Starter 150 | 70b2614b-639a-4de2-8c11-3851d36d6f8c: 'Blog Categories' # Blog Categories 151 | 163c895f-0c05-4845-80e2-043b2346d830: Address # Address 152 | 0413e9dc-1c8a-4221-b81d-c9f6c0594cbd: Hero # Hero 153 | 697da4c2-e02f-4f49-9d76-e22609ceda25: Page # Page 154 | 998bf681-e007-4f7e-a525-863e01a38560: Blog # Blog 155 | 5358db38-27c0-49b4-b9d3-e68326e79da2: Image # Image 156 | 0922209d-2911-419e-82cf-b05113a35f6e: Wide # Wide 157 | a06e695e-af7f-4a1b-84a7-575ec935e293: Video # Video 158 | aef20f9c-5c84-4c50-8a18-acf0452db62f: Guestbook # Guestbook 159 | b5b7bfc5-a39e-4d1a-89be-51f0afeb5ce0: Avatar # Avatar 160 | b579b454-12bd-4710-a436-c26907141957: Full # Full 161 | bd99645a-4524-40e4-83dc-c6b12e023bab: Global # Global 162 | c4b4f50d-7d62-4a4c-bbf8-7aae00fd82c7: Pages # Pages 163 | c7d2eb61-cdde-4a76-88a9-eb30ddcf155b: Guestbook # Guestbook 164 | cdad2202-5d55-49ba-995e-962503f5887f: Home # Home 165 | d4f0917b-edcf-4ef0-a98b-661ff27384ad: Page # Page 166 | d55d172f-8b8f-4bb7-b680-31249981c350: Global # Global 167 | d390347a-09cf-4b70-a3b4-bc3cca520623: Category # Category 168 | f7576aa3-c155-4b54-82c9-8cb8c93495c9: Callout # Callout 169 | plugins: 170 | ckeditor: 171 | edition: standard 172 | enabled: true 173 | schemaVersion: 3.0.0.0 174 | system: 175 | edition: solo 176 | live: true 177 | name: 'Next Starter' 178 | retryDuration: null 179 | schemaVersion: 5.7.0.3 180 | timeZone: America/Los_Angeles 181 | -------------------------------------------------------------------------------- /backend/config/project/sections/blog--998bf681-e007-4f7e-a525-863e01a38560.yaml: -------------------------------------------------------------------------------- 1 | defaultPlacement: end 2 | enableVersioning: true 3 | entryTypes: 4 | - 697da4c2-e02f-4f49-9d76-e22609ceda25 # Page 5 | handle: blog 6 | maxAuthors: 1 7 | name: Blog 8 | previewTargets: 9 | - 10 | __assoc__: 11 | - 12 | - label 13 | - 'Blog index' 14 | - 15 | - urlFormat 16 | - '{url}' 17 | - 18 | - refresh 19 | - '1' 20 | propagationMethod: all 21 | siteSettings: 22 | 58ccb571-6c72-449f-8e7b-6f43c5d416b3: # Next Starter 23 | enabledByDefault: true 24 | hasUrls: true 25 | template: null 26 | uriFormat: blog 27 | type: single 28 | -------------------------------------------------------------------------------- /backend/config/project/sections/blogCategories--70b2614b-639a-4de2-8c11-3851d36d6f8c.yaml: -------------------------------------------------------------------------------- 1 | defaultPlacement: end 2 | enableVersioning: true 3 | entryTypes: 4 | - 5dd08fc8-3ea4-4582-952f-4c0f7ebf409e # Category 5 | handle: blogCategories 6 | maxAuthors: 1 7 | name: 'Blog Categories' 8 | previewTargets: 9 | - 10 | __assoc__: 11 | - 12 | - label 13 | - 'Category feed' 14 | - 15 | - urlFormat 16 | - '{url}' 17 | - 18 | - refresh 19 | - '1' 20 | propagationMethod: all 21 | siteSettings: 22 | 58ccb571-6c72-449f-8e7b-6f43c5d416b3: # Next Starter 23 | enabledByDefault: true 24 | hasUrls: true 25 | template: null 26 | uriFormat: 'categories/{slug}' 27 | structure: 28 | maxLevels: null 29 | uid: 3fa989b7-cc6c-402a-b995-5698a2b688ea 30 | type: structure 31 | -------------------------------------------------------------------------------- /backend/config/project/sections/blogPosts--6ba54824-1c3f-454f-8830-f6cea379bee4.yaml: -------------------------------------------------------------------------------- 1 | defaultPlacement: end 2 | enableVersioning: true 3 | entryTypes: 4 | - 697da4c2-e02f-4f49-9d76-e22609ceda25 # Page 5 | handle: blogPosts 6 | maxAuthors: 1 7 | name: 'Blog Posts' 8 | previewTargets: 9 | - 10 | __assoc__: 11 | - 12 | - label 13 | - 'Blog feed' 14 | - 15 | - urlFormat 16 | - '{url}' 17 | - 18 | - refresh 19 | - '1' 20 | propagationMethod: all 21 | siteSettings: 22 | 58ccb571-6c72-449f-8e7b-6f43c5d416b3: # Next Starter 23 | enabledByDefault: true 24 | hasUrls: true 25 | template: null 26 | uriFormat: 'blog/{slug}' 27 | type: channel 28 | -------------------------------------------------------------------------------- /backend/config/project/sections/global--bd99645a-4524-40e4-83dc-c6b12e023bab.yaml: -------------------------------------------------------------------------------- 1 | defaultPlacement: end 2 | enableVersioning: true 3 | entryTypes: 4 | - d55d172f-8b8f-4bb7-b680-31249981c350 # Global 5 | handle: global 6 | maxAuthors: 1 7 | name: Global 8 | propagationMethod: all 9 | siteSettings: 10 | 58ccb571-6c72-449f-8e7b-6f43c5d416b3: # Next Starter 11 | enabledByDefault: true 12 | hasUrls: false 13 | template: null 14 | uriFormat: null 15 | type: single 16 | -------------------------------------------------------------------------------- /backend/config/project/sections/guestbook--aef20f9c-5c84-4c50-8a18-acf0452db62f.yaml: -------------------------------------------------------------------------------- 1 | defaultPlacement: end 2 | enableVersioning: true 3 | entryTypes: 4 | - 697da4c2-e02f-4f49-9d76-e22609ceda25 # Page 5 | handle: guestbook 6 | maxAuthors: 1 7 | name: Guestbook 8 | previewTargets: 9 | - 10 | __assoc__: 11 | - 12 | - label 13 | - 'Primary entry page' 14 | - 15 | - urlFormat 16 | - '{url}' 17 | - 18 | - refresh 19 | - '1' 20 | propagationMethod: all 21 | siteSettings: 22 | 58ccb571-6c72-449f-8e7b-6f43c5d416b3: # Next Starter 23 | enabledByDefault: true 24 | hasUrls: true 25 | template: null 26 | uriFormat: guestbook 27 | type: single 28 | -------------------------------------------------------------------------------- /backend/config/project/sections/guestbookPosts--3e2a1a6f-88ea-477b-936d-39393f2d594f.yaml: -------------------------------------------------------------------------------- 1 | defaultPlacement: end 2 | enableVersioning: true 3 | entryTypes: 4 | - 25b2446d-c1d2-4aa1-8184-3147e617f069 # Text 5 | handle: guestbookPosts 6 | maxAuthors: 1 7 | name: 'Guestbook Posts' 8 | previewTargets: 9 | - 10 | __assoc__: 11 | - 12 | - label 13 | - 'Primary entry page' 14 | - 15 | - urlFormat 16 | - '{url}' 17 | - 18 | - refresh 19 | - '1' 20 | propagationMethod: all 21 | siteSettings: 22 | 58ccb571-6c72-449f-8e7b-6f43c5d416b3: # Next Starter 23 | enabledByDefault: true 24 | hasUrls: true 25 | template: null 26 | uriFormat: 'posts/{slug}' 27 | type: channel 28 | -------------------------------------------------------------------------------- /backend/config/project/sections/home--cdad2202-5d55-49ba-995e-962503f5887f.yaml: -------------------------------------------------------------------------------- 1 | defaultPlacement: end 2 | enableVersioning: true 3 | entryTypes: 4 | - 697da4c2-e02f-4f49-9d76-e22609ceda25 # Page 5 | handle: home 6 | maxAuthors: 1 7 | name: Home 8 | previewTargets: 9 | - 10 | __assoc__: 11 | - 12 | - label 13 | - Homepage 14 | - 15 | - urlFormat 16 | - '{url}' 17 | - 18 | - refresh 19 | - '1' 20 | propagationMethod: all 21 | siteSettings: 22 | 58ccb571-6c72-449f-8e7b-6f43c5d416b3: # Next Starter 23 | enabledByDefault: true 24 | hasUrls: true 25 | template: null 26 | uriFormat: __home__ 27 | type: single 28 | -------------------------------------------------------------------------------- /backend/config/project/sections/pages--c4b4f50d-7d62-4a4c-bbf8-7aae00fd82c7.yaml: -------------------------------------------------------------------------------- 1 | defaultPlacement: end 2 | enableVersioning: true 3 | entryTypes: 4 | - 697da4c2-e02f-4f49-9d76-e22609ceda25 # Page 5 | handle: pages 6 | maxAuthors: 1 7 | name: Pages 8 | previewTargets: 9 | - 10 | __assoc__: 11 | - 12 | - label 13 | - 'Primary entry page' 14 | - 15 | - urlFormat 16 | - '{url}' 17 | - 18 | - refresh 19 | - '1' 20 | propagationMethod: all 21 | siteSettings: 22 | 58ccb571-6c72-449f-8e7b-6f43c5d416b3: # Next Starter 23 | enabledByDefault: true 24 | hasUrls: true 25 | template: null 26 | uriFormat: '{{ parent ? parent.uri : null }}/{slug}' 27 | structure: 28 | maxLevels: null 29 | uid: 97fc32ea-6e86-45dd-b3dd-97cdc4360518 30 | type: structure 31 | -------------------------------------------------------------------------------- /backend/config/project/siteGroups/6d0d3aa4-84be-441d-8fcc-2ab2d6db2503.yaml: -------------------------------------------------------------------------------- 1 | name: Starters 2 | -------------------------------------------------------------------------------- /backend/config/project/sites/default--58ccb571-6c72-449f-8e7b-6f43c5d416b3.yaml: -------------------------------------------------------------------------------- 1 | baseUrl: $PRIMARY_SITE_URL 2 | enabled: true 3 | handle: default 4 | hasUrls: true 5 | language: en-US 6 | name: 'Next Starter' 7 | primary: true 8 | siteGroup: 6d0d3aa4-84be-441d-8fcc-2ab2d6db2503 # Starters 9 | sortOrder: 1 10 | -------------------------------------------------------------------------------- /backend/config/project/users/fieldLayouts/7e6be4ec-feec-4e60-91d5-3669ac76ea98.yaml: -------------------------------------------------------------------------------- 1 | tabs: 2 | - 3 | elementCondition: null 4 | elements: 5 | - 6 | autocapitalize: true 7 | autocomplete: false 8 | autocorrect: true 9 | class: null 10 | dateAdded: '2024-10-29T05:19:03-07:00' 11 | disabled: false 12 | elementCondition: null 13 | id: null 14 | includeInCards: false 15 | inputType: null 16 | instructions: null 17 | label: null 18 | max: null 19 | min: null 20 | name: null 21 | orientation: null 22 | placeholder: null 23 | providesThumbs: false 24 | readonly: false 25 | requirable: false 26 | size: null 27 | step: null 28 | tip: null 29 | title: null 30 | type: craft\fieldlayoutelements\users\UsernameField 31 | uid: bcfbc6ae-0f10-4d19-a831-96d9991f5c42 32 | userCondition: null 33 | warning: null 34 | width: 100 35 | - 36 | attribute: fullName 37 | autocapitalize: true 38 | autocomplete: false 39 | autocorrect: true 40 | class: null 41 | dateAdded: '2024-10-29T05:19:03-07:00' 42 | disabled: false 43 | elementCondition: null 44 | id: null 45 | includeInCards: false 46 | inputType: null 47 | instructions: null 48 | label: null 49 | max: null 50 | min: null 51 | name: null 52 | orientation: null 53 | placeholder: null 54 | providesThumbs: false 55 | readonly: false 56 | requirable: true 57 | required: false 58 | size: null 59 | step: null 60 | tip: null 61 | title: null 62 | type: craft\fieldlayoutelements\users\FullNameField 63 | uid: f422e5bb-6963-4083-a980-269180f8b049 64 | userCondition: null 65 | warning: null 66 | width: 100 67 | - 68 | dateAdded: '2024-10-29T05:19:03-07:00' 69 | elementCondition: null 70 | id: null 71 | includeInCards: false 72 | instructions: null 73 | label: null 74 | orientation: null 75 | providesThumbs: false 76 | requirable: false 77 | tip: null 78 | type: craft\fieldlayoutelements\users\PhotoField 79 | uid: da2825c5-4641-42e8-b641-3ff7eb1547c4 80 | userCondition: null 81 | warning: null 82 | width: 100 83 | - 84 | autocapitalize: true 85 | autocomplete: false 86 | autocorrect: true 87 | class: null 88 | dateAdded: '2024-10-29T05:19:03-07:00' 89 | disabled: false 90 | elementCondition: null 91 | id: null 92 | includeInCards: false 93 | inputType: null 94 | instructions: null 95 | label: null 96 | max: null 97 | min: null 98 | name: null 99 | orientation: null 100 | placeholder: null 101 | providesThumbs: false 102 | readonly: false 103 | requirable: false 104 | size: null 105 | step: null 106 | tip: null 107 | title: null 108 | type: craft\fieldlayoutelements\users\EmailField 109 | uid: 55a7f540-710d-49c0-8629-ab6739f519ec 110 | userCondition: null 111 | width: 100 112 | name: Content 113 | uid: 76592e9a-74ef-4c45-93fd-c3bd8838c8bb 114 | userCondition: null 115 | -------------------------------------------------------------------------------- /backend/config/project/users/users.yaml: -------------------------------------------------------------------------------- 1 | allowPublicRegistration: false 2 | defaultGroup: null 3 | photoSubpath: null 4 | photoVolumeUid: null 5 | require2fa: false 6 | requireEmailVerification: true 7 | -------------------------------------------------------------------------------- /backend/config/project/volumes/media--7b9364bd-2215-495a-8b0b-2280df887a8b.yaml: -------------------------------------------------------------------------------- 1 | altTranslationKeyFormat: null 2 | altTranslationMethod: none 3 | fieldLayouts: 4 | 4278d19f-4684-4b69-accb-a46c7d8a5c32: 5 | tabs: 6 | - 7 | elementCondition: null 8 | elements: 9 | - 10 | autocapitalize: true 11 | autocomplete: false 12 | autocorrect: true 13 | class: null 14 | dateAdded: '2024-08-20T08:42:19+00:00' 15 | disabled: false 16 | elementCondition: null 17 | id: null 18 | includeInCards: false 19 | inputType: null 20 | instructions: null 21 | label: null 22 | max: null 23 | min: null 24 | name: null 25 | orientation: null 26 | placeholder: null 27 | providesThumbs: false 28 | readonly: false 29 | requirable: false 30 | size: null 31 | step: null 32 | tip: null 33 | title: null 34 | type: craft\fieldlayoutelements\assets\AssetTitleField 35 | uid: 490cde70-7592-48e6-bfb1-2128f1a653c0 36 | userCondition: null 37 | warning: null 38 | width: 100 39 | - 40 | attribute: alt 41 | class: null 42 | cols: null 43 | dateAdded: '2024-08-20T08:44:45+00:00' 44 | disabled: false 45 | elementCondition: null 46 | id: null 47 | includeInCards: false 48 | instructions: null 49 | label: null 50 | name: null 51 | orientation: null 52 | placeholder: null 53 | providesThumbs: false 54 | readonly: false 55 | requirable: true 56 | required: false 57 | rows: null 58 | tip: null 59 | title: null 60 | type: craft\fieldlayoutelements\assets\AltField 61 | uid: a66092a1-85e7-4d2f-bbc7-cac60a707624 62 | userCondition: null 63 | warning: null 64 | width: 100 65 | name: Content 66 | uid: b16c4140-39f5-4220-b0a1-e83126b7daaf 67 | userCondition: null 68 | fs: cloud 69 | handle: media 70 | name: Media 71 | sortOrder: 1 72 | subpath: img 73 | titleTranslationKeyFormat: null 74 | titleTranslationMethod: site 75 | transformFs: '' 76 | transformSubpath: img/transforms 77 | -------------------------------------------------------------------------------- /backend/config/routes.php: -------------------------------------------------------------------------------- 1 | 'graphql/api' 14 | ]; 15 | -------------------------------------------------------------------------------- /backend/craft: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 14 | exit($exitCode); 15 | -------------------------------------------------------------------------------- /backend/craft-cloud.yaml: -------------------------------------------------------------------------------- 1 | # Craft Cloud configuration file 2 | # https://craftcms.com/knowledge-base/cloud-config 3 | php-version: '8.2' 4 | -------------------------------------------------------------------------------- /backend/modules/authorinfo/Module.php: -------------------------------------------------------------------------------- 1 | getRequest()->getIsConsoleRequest()) { 27 | $this->controllerNamespace = 'modules\\authorinfo\\console\\controllers'; 28 | } else { 29 | $this->controllerNamespace = 'modules\\authorinfo\\controllers'; 30 | } 31 | 32 | Event::on( 33 | TypeManager::class, 34 | TypeManager::EVENT_DEFINE_GQL_TYPE_FIELDS, 35 | [$this, 'handleDefineGqlTypeFields'] 36 | ); 37 | } 38 | 39 | /** 40 | * Get the admin user, caching the result 41 | */ 42 | private function getAdminUser(): ?User 43 | { 44 | if ($this->adminUser === null) { 45 | $this->adminUser = User::find() 46 | ->admin(true) 47 | ->status(null) 48 | ->orderBy(['elements.id' => SORT_ASC]) 49 | ->one(); 50 | 51 | if ($this->adminUser) { 52 | Craft::info( 53 | "Found admin user: ID: {$this->adminUser->id}, Name: {$this->adminUser->fullName}", 54 | __METHOD__ 55 | ); 56 | } else { 57 | Craft::warning('No admin user found', __METHOD__); 58 | } 59 | } 60 | 61 | return $this->adminUser; 62 | } 63 | 64 | /** 65 | * Handle the DefineGqlTypeFields event 66 | */ 67 | public function handleDefineGqlTypeFields(DefineGqlTypeFieldsEvent $event): void 68 | { 69 | if ($event->typeName !== 'EntryInterface') { 70 | return; 71 | } 72 | 73 | $event->fields['authorId'] = [ 74 | 'name' => 'authorId', 75 | 'type' => Type::int(), 76 | 'description' => 'The entry author\'s ID', 77 | 'resolve' => function($source) { 78 | try { 79 | if ($source->getSection()->type === 'single') { 80 | return $this->getAdminUser()?->id; 81 | } 82 | return $source->authorId; 83 | } catch (\Throwable $e) { 84 | Craft::error("Error resolving authorId: {$e->getMessage()}", __METHOD__); 85 | return null; 86 | } 87 | } 88 | ]; 89 | 90 | $event->fields['authorName'] = [ 91 | 'name' => 'authorName', 92 | 'type' => Type::string(), 93 | 'description' => 'The entry author\'s name', 94 | 'resolve' => function($source) { 95 | try { 96 | if ($source->getSection()->type === 'single') { 97 | return $this->getAdminUser()?->fullName; 98 | } 99 | return $source->getAuthor()?->fullName; 100 | } catch (\Throwable $e) { 101 | Craft::error("Error resolving authorName: {$e->getMessage()}", __METHOD__); 102 | return null; 103 | } 104 | } 105 | ]; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /backend/storage/.gitignore: -------------------------------------------------------------------------------- 1 | backups 2 | composer-backups 3 | config-backups 4 | logs 5 | runtime 6 | -------------------------------------------------------------------------------- /backend/storage/config-deltas/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/templates/.gitignore: -------------------------------------------------------------------------------- 1 | # Hey! 2 | # Craft is running in “headless” mode, so it doesn’t make much use of this 3 | # directory. The `_partials/` folder, however, holds a few templates that 4 | # may be used to render a nested entry within a CKEditor field. 5 | # 6 | # - Headless mode: https://craftcms.com/docs/5.x/reference/config/general.html#headlessmode 7 | # - Element partials: https://craftcms.com/docs/5.x/system/elements.html#element-partials 8 | # - CKEditor: https://github.com/craftcms/ckeditor 9 | # 10 | # (Also—this file isn’t here to actually ignore anything! You are free to delete it.) 11 | -------------------------------------------------------------------------------- /backend/templates/_partials/entry/callout.twig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/templates/_partials/entry/image.twig: -------------------------------------------------------------------------------- 1 | {% import '_utilities/image' as img %} 2 | 3 |
4 | {% set assets = entry.image.eagerly().all() %} 5 | {{ img.pictures(assets) }} 6 |
7 | -------------------------------------------------------------------------------- /backend/templates/_partials/entry/text.twig: -------------------------------------------------------------------------------- 1 |
2 | {{ entry.textBlock|md }} 3 |
4 | -------------------------------------------------------------------------------- /backend/templates/_partials/entry/video.twig: -------------------------------------------------------------------------------- 1 |
2 | {{ entry.embedCode|raw }} 3 |
4 | -------------------------------------------------------------------------------- /backend/templates/_utilities/image.twig: -------------------------------------------------------------------------------- 1 | {% macro pictures(photos, decorative = false) %} 2 | 3 | {% for photo in photos %} 4 | {% if photo.extension == 'svg' %} 5 | {% set alt = decorative ? null : (photo.alt ?? null) %} 6 | {% set role = decorative ? null : 'img' %} 7 | {{ svg(photo)|attr({ role: role, 'aria-label': alt }) }} 8 | {% else %} 9 | {% set outputWidths = [640, 1024, 1920] %} 10 | {% set srcset = [] %} 11 | {% for outputWidth in outputWidths %} 12 | {% if outputWidth <= photo.width %} 13 | {% set srcset = srcset | merge([photo.url({ width: outputWidth }) ~ ' ' ~ outputWidth ~ 'w']) %} 14 | {% endif %} 15 | {% endfor %} 16 | 17 | 20 | 27 | 28 | {% endif %} 29 | {% endfor %} 30 | {% endmacro %} 31 | -------------------------------------------------------------------------------- /backend/web/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | 4 | # Send would-be 404 requests to Craft 5 | RewriteCond %{REQUEST_FILENAME} !-f 6 | RewriteCond %{REQUEST_FILENAME} !-d 7 | RewriteCond %{REQUEST_URI} !^/(favicon\.ico|apple-touch-icon.*\.png)$ [NC] 8 | RewriteRule (.+) index.php?p=$1 [QSA,L] 9 | 10 | -------------------------------------------------------------------------------- /backend/web/cpresources/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/web/index.php: -------------------------------------------------------------------------------- 1 | run(); 13 | -------------------------------------------------------------------------------- /backend/web/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | SITE_NAME="Next Starter" 2 | DEV_MODE=true 3 | 4 | # Site URLs 5 | BASE_URL="https://starter-next.ddev.site" 6 | CRAFT_DOMAIN=api.starter-next.ddev.site 7 | CRAFT_URL=https://$CRAFT_DOMAIN 8 | 9 | # GraphQL API 10 | GRAPHQL_TOKEN="" 11 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | .env 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app), with the express intention of being bundled with the adjacent `backend/` directory. 2 | 3 | > [!WARNING] 4 | > Moving this folder (or any of the files within it) may require additional configuration changes in DDEV or Craft. If you have any questions about its structure, refer to this repository’s root `README.md` file or create an [issue](https://github.com/craftcms/starter-next/issues)! 5 | 6 | ## Learn More 7 | 8 | To learn more about Next.js, take a look at the following resources: 9 | 10 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 11 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 12 | -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: [process.env.CRAFT_DOMAIN], 5 | remotePatterns: [ 6 | { 7 | protocol: 'https', 8 | hostname: process.env.CRAFT_DOMAIN, 9 | pathname: '/uploads/**', 10 | } 11 | ] 12 | }, 13 | env: { 14 | SITE_NAME: process.env.SITE_NAME, 15 | CRAFT_URL: process.env.CRAFT_URL, 16 | BASE_URL: process.env.BASE_URL, 17 | GRAPHQL_TOKEN: process.env.GRAPHQL_TOKEN 18 | }, 19 | reactStrictMode: true, 20 | poweredByHeader: false, 21 | compress: true, 22 | logging: process.env.NODE_ENV === 'development' ? { 23 | fetches: { 24 | fullUrl: true, 25 | }, 26 | } : undefined, 27 | turbopack: { 28 | rules: { 29 | '*.css': ['style-loader', 'css-loader'] 30 | } 31 | } 32 | } 33 | 34 | module.exports = nextConfig 35 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starter-next", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@tailwindcss/postcss": "^4.1.4", 13 | "next": "^15.3.0", 14 | "postcss": "^8.5.3", 15 | "react": "^19.1.0", 16 | "react-dom": "^19.1.0", 17 | "tailwindcss": "^4.1.4" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.26.10", 21 | "babel-plugin-transform-react-remove-prop-types": "^0.4.24", 22 | "eslint": "^9", 23 | "eslint-config-next": "^15.3.0", 24 | "@tailwindcss/forms": "^0.5.10", 25 | "@tailwindcss/typography": "^0.5.16" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /frontend/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/[...slug]/page.jsx: -------------------------------------------------------------------------------- 1 | import { createPage } from '../../lib/createPage' 2 | import { PAGE_QUERY } from '../../queries/pages' 3 | 4 | export const dynamic = 'force-static' 5 | export const revalidate = 3600 6 | 7 | const transform = (data, isPreview = false) => { 8 | const entry = data?.entry || data?.entries?.[0] || {} 9 | 10 | return { 11 | title: entry.title || '', 12 | pageSubheading: entry.pageSubheading || '', 13 | pageContent: entry.pageContent || '', 14 | image: entry.image ? [entry.image] : undefined, 15 | ancestors: entry.ancestors || [], 16 | children: entry.children || [], 17 | ...entry 18 | } 19 | } 20 | 21 | export default createPage(PAGE_QUERY, transform, null, { 22 | variables: ({ params }) => ({ 23 | uri: params?.slug?.join('/') || '' 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /frontend/src/app/blog/[slug]/page.jsx: -------------------------------------------------------------------------------- 1 | import { createPage } from '../../../lib/createPage' 2 | import { BLOG_POSTS_QUERY } from '../../../queries/blogPosts' 3 | 4 | export const dynamic = 'force-static' 5 | export const revalidate = 3600 6 | 7 | const transform = (data, isPreview = false) => { 8 | const entry = data?.entry || data?.blogPostsEntries?.[0] || {} 9 | 10 | return { 11 | title: entry.title || '', 12 | pageSubheading: entry.pageSubheading || '', 13 | pageContent: entry.pageContent || '', 14 | authorName: entry.authorName || '', 15 | authorId: entry.authorId || '', 16 | sectionHandle: entry.sectionHandle || '', 17 | postDate: entry.postDate || '', 18 | image: entry.image ? [entry.image] : undefined, 19 | next: entry.next || null, 20 | prev: entry.prev || null, 21 | category: entry.category || null, 22 | ...entry 23 | } 24 | } 25 | 26 | export default createPage(BLOG_POSTS_QUERY, transform, null, { 27 | variables: ({ params }) => ({ 28 | slug: [params?.slug].filter(Boolean) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /frontend/src/app/blog/page.jsx: -------------------------------------------------------------------------------- 1 | import { fetchGraphQL } from '../../lib/graphql' 2 | import { BLOG_QUERY } from '../../queries/blog' 3 | import BlogList from '../../components/BlogList' 4 | 5 | const ITEMS_PER_PAGE = 4 6 | 7 | async function getData(page = 1) { 8 | const data = await fetchGraphQL(BLOG_QUERY, { 9 | limit: ITEMS_PER_PAGE, 10 | offset: (page - 1) * ITEMS_PER_PAGE 11 | }) 12 | 13 | if (!data?.blogEntries?.[0]) { 14 | throw new Error('No blog entries found') 15 | } 16 | 17 | return { 18 | ...data, 19 | totalPages: Math.ceil((data?.entryCount || 0) / ITEMS_PER_PAGE) 20 | } 21 | } 22 | 23 | export const dynamic = 'force-dynamic' 24 | 25 | export default async function BlogPage({ searchParams }) { 26 | const currentPage = parseInt(searchParams?.page || '1', 10) 27 | 28 | const data = await getData(currentPage) 29 | 30 | return 36 | } -------------------------------------------------------------------------------- /frontend/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craftcms/starter-next/b0031de05399b747b95623d99f387b62f331e511/frontend/src/app/favicon.ico -------------------------------------------------------------------------------- /frontend/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @plugin "@tailwindcss/forms"; 4 | @plugin "@tailwindcss/typography"; 5 | 6 | @source "../../../backend/templates"; 7 | @source "./safelist.txt"; 8 | 9 | 10 | @layer base { 11 | :focus-visible { 12 | @apply outline-none ring-2 ring-red-600 ring-offset-2; 13 | } 14 | a:focus-visible, 15 | button:focus-visible, 16 | input:focus-visible, 17 | select:focus-visible, 18 | textarea:focus-visible { 19 | @apply outline-none ring-2 ring-red-600 ring-offset-2 rounded; 20 | } 21 | } 22 | 23 | nav a { 24 | @apply relative; 25 | } 26 | 27 | nav a:focus-visible { 28 | @apply outline-none ring-2 ring-red-600 ring-offset-2 rounded-sm z-10; 29 | } -------------------------------------------------------------------------------- /frontend/src/app/guestbook/page.jsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation' 2 | import { fetchGraphQL } from '../../lib/graphql' 3 | import { GUESTBOOK_QUERY } from '../../queries/guestbook' 4 | import { Content } from '../../components/Content' 5 | import { GuestbookInteractive } from '../../components/GuestbookInteractive' 6 | 7 | export const dynamic = 'force-static' 8 | export const revalidate = 3600 9 | 10 | const transform = (data) => { 11 | if (!data?.guestbookEntries?.[0]) return notFound() 12 | 13 | const entry = data.guestbookEntries[0] 14 | return { 15 | ...entry, 16 | title: entry.title || '', 17 | pageSubheading: entry.pageSubheading || '', 18 | pageContent: entry.pageContent || '', 19 | authorId: entry.authorId 20 | } 21 | } 22 | 23 | export default async function Page() { 24 | const data = await fetchGraphQL(GUESTBOOK_QUERY) 25 | 26 | const pageData = transform(data) 27 | 28 | return ( 29 | <> 30 | 31 |
32 | 33 |
34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/app/layout.jsx: -------------------------------------------------------------------------------- 1 | import { GLOBALS_QUERY } from '../queries/globals' 2 | import { fetchGraphQL } from '../lib/graphql' 3 | import { FlashProvider } from '../lib/flashes' 4 | import { Alert } from '../components/Alert' 5 | import Header from '../components/Header' 6 | import Footer from '../components/Footer' 7 | import "./globals.css" 8 | import { SkipLink } from '../components/SkipLink' 9 | 10 | export const metadata = { 11 | title: process.env.SITE_NAME, 12 | description: 'A minimal, production-ready starter for Next.js 15 and Craft CMS' 13 | } 14 | 15 | export default async function RootLayout({ children }) { 16 | const data = await fetchGraphQL(GLOBALS_QUERY, {}, { 17 | cache: 'force-cache', 18 | next: { revalidate: 3600 } 19 | }) 20 | 21 | const globals = data?.globalEntries?.[0] || {} 22 | const pages = data?.pagesEntries || [] 23 | const siteName = process.env.SITE_NAME || '' 24 | 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 | 37 | 38 | {children} 39 | 40 |
41 |